This commit is contained in:
@@ -42,6 +42,8 @@ h1 { max-width: 650px; margin: 0; font-size: clamp(30px, 4vw, 45px); font-style:
|
||||
.dek p { margin: 0; }
|
||||
|
||||
.activity-section { min-height: 370px; padding: 48px 0 72px; }
|
||||
.homepage-discovery-grid { display: grid; grid-template-columns: minmax(0, 1.65fr) minmax(290px, 1fr); gap: 36px; align-items: start; }
|
||||
.homepage-panel { min-width: 0; }
|
||||
.section-heading { display: flex; justify-content: space-between; padding-bottom: 22px; border-bottom: 1px solid var(--rule); color: var(--muted); font-size: 10px; }
|
||||
.timeline { position: relative; margin: 16px 0 0 7px; padding-left: 30px; border-left: 1px solid var(--rule); }
|
||||
.timeline-entry { position: relative; padding: 0 0 30px; }
|
||||
@@ -75,6 +77,14 @@ h1 { max-width: 650px; margin: 0; font-size: clamp(30px, 4vw, 45px); font-style:
|
||||
.activity-more-button:focus-visible { border-color: var(--teal); color: var(--teal); }
|
||||
.activity-more-button:disabled { cursor: default; opacity: .55; }
|
||||
|
||||
.featured-project-list { border-bottom: 1px solid var(--rule); }
|
||||
.featured-project { padding: 22px 0 24px; }
|
||||
.featured-project + .featured-project { border-top: 1px solid rgba(39,48,57,.65); }
|
||||
.featured-project h2 { margin: 10px 0 5px; font-size: 21px; line-height: 1.3; }
|
||||
.featured-project p { margin: 0 0 10px; color: var(--muted); font-size: 14px; line-height: 1.55; }
|
||||
.featured-projects-empty { margin: 0; padding: 24px 0; color: var(--muted); }
|
||||
.featured-projects-link { display: inline-block; margin-top: 22px; color: var(--teal); font: 500 10px var(--mono); letter-spacing: .12em; text-transform: uppercase; }
|
||||
|
||||
.card-grid { display: grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: 1px; background: var(--rule); border: 1px solid var(--rule); }
|
||||
.entry-card { min-height: 245px; padding: 28px; background: var(--bg); }
|
||||
.entry-tags { display: flex; flex-wrap: wrap; gap: 7px; margin: 16px 0; }
|
||||
@@ -301,6 +311,7 @@ h1 { max-width: 650px; margin: 0; font-size: clamp(30px, 4vw, 45px); font-style:
|
||||
.header-meta { display: none; }
|
||||
.hero, .hero.compact { padding-block: 44px; }
|
||||
h1 { font-size: 30px; }
|
||||
.homepage-discovery-grid { grid-template-columns: 1fr; gap: 48px; }
|
||||
.card-grid { grid-template-columns: 1fr; }
|
||||
.tag-directory { grid-template-columns: 1fr; }
|
||||
.tag-filter { padding-block: 18px; }
|
||||
|
||||
@@ -3,3 +3,4 @@ repository = https://git.labyricorn.com/Labyricorn/thinkloom-openai-hackathon.gi
|
||||
web_url = https://git.labyricorn.com/Labyricorn/thinkloom-openai-hackathon
|
||||
api_url = https://git.labyricorn.com/api/v1/repos/Labyricorn/thinkloom-openai-hackathon
|
||||
branch = main
|
||||
featured_order = 10
|
||||
|
||||
@@ -30,6 +30,14 @@ type = select
|
||||
choices = active, released, maintained, archived
|
||||
choice_labels = Active, Released, Maintained, Archived
|
||||
|
||||
[fields.featured]
|
||||
label = Feature on homepage
|
||||
type = boolean
|
||||
|
||||
[fields.featured_order]
|
||||
label = Homepage feature order
|
||||
type = integer
|
||||
|
||||
[fields.started]
|
||||
label = Started
|
||||
type = date
|
||||
|
||||
+21
-12
@@ -123,13 +123,13 @@ def validate_url(value: str, field: str) -> str:
|
||||
return value.rstrip("/")
|
||||
|
||||
|
||||
def load_registry(site_root: Path) -> list[dict[str, str]]:
|
||||
def load_registry(site_root: Path) -> list[dict[str, str | int]]:
|
||||
registry_path = site_root / "configs" / "project-sources.ini"
|
||||
parser = configparser.ConfigParser(interpolation=None)
|
||||
if not parser.read(registry_path, encoding="utf-8"):
|
||||
raise ProjectSourceError(f"project source registry is missing: {registry_path}")
|
||||
|
||||
sources: list[dict[str, str]] = []
|
||||
sources: list[dict[str, str | int]] = []
|
||||
for project_id in parser.sections():
|
||||
if not SLUG_RE.fullmatch(project_id):
|
||||
raise ProjectSourceError(f"invalid project id in registry: {project_id}")
|
||||
@@ -141,15 +141,21 @@ def load_registry(site_root: Path) -> list[dict[str, str]]:
|
||||
branch = section["branch"].strip()
|
||||
if not re.fullmatch(r"[A-Za-z0-9._/-]+", branch) or ".." in branch:
|
||||
raise ProjectSourceError(f"{project_id}: invalid branch")
|
||||
sources.append(
|
||||
{
|
||||
"project_id": project_id,
|
||||
"repository": validate_url(section["repository"].strip(), "repository"),
|
||||
"web_url": validate_url(section["web_url"].strip(), "web_url"),
|
||||
"api_url": validate_url(section["api_url"].strip(), "api_url"),
|
||||
"branch": branch,
|
||||
}
|
||||
)
|
||||
source: dict[str, str | int] = {
|
||||
"project_id": project_id,
|
||||
"repository": validate_url(section["repository"].strip(), "repository"),
|
||||
"web_url": validate_url(section["web_url"].strip(), "web_url"),
|
||||
"api_url": validate_url(section["api_url"].strip(), "api_url"),
|
||||
"branch": branch,
|
||||
}
|
||||
if "featured_order" in section:
|
||||
try:
|
||||
source["featured_order"] = int(section["featured_order"])
|
||||
except ValueError as exc:
|
||||
raise ProjectSourceError(
|
||||
f"{project_id}: featured_order must be an integer"
|
||||
) from exc
|
||||
sources.append(source)
|
||||
if not sources:
|
||||
raise ProjectSourceError("project source registry is empty")
|
||||
return sources
|
||||
@@ -566,7 +572,7 @@ def append_fields(original: bytes, fields: dict[str, Any]) -> bytes:
|
||||
|
||||
|
||||
def materialize(
|
||||
source: dict[str, str],
|
||||
source: dict[str, str | int],
|
||||
destination_root: Path,
|
||||
files: dict[str, bytes],
|
||||
records: dict[str, dict[str, str]],
|
||||
@@ -630,6 +636,9 @@ def materialize(
|
||||
"repository_language_tags": repository_language_tags,
|
||||
"taxonomy_tags": taxonomy_tags,
|
||||
}
|
||||
if "featured_order" in source:
|
||||
project_fields["featured"] = "yes"
|
||||
project_fields["featured_order"] = source["featured_order"]
|
||||
|
||||
for source_path, data in files.items():
|
||||
relative = PurePosixPath(source_path).relative_to(".labyricorn")
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=Inter:wght@400;500&family=Playfair+Display:ital,wght@0,600;0,700;1,500;1,600&display=swap" rel="stylesheet">
|
||||
<link rel="icon" href="{{ '/static/favicon.svg'|url }}" type="image/svg+xml">
|
||||
<link rel="stylesheet" href="{{ '/static/style.css'|url }}?v=10">
|
||||
<link rel="stylesheet" href="{{ '/static/style.css'|url }}?v=11">
|
||||
<script src="{{ '/static/tags.js'|url }}?v=1" defer></script>
|
||||
<script src="{{ '/static/activity.js'|url }}?v=1" defer></script>
|
||||
</head>
|
||||
|
||||
+62
-35
@@ -8,42 +8,69 @@
|
||||
|
||||
<section class="activity-section">
|
||||
<div class="shell">
|
||||
<div class="section-heading">
|
||||
<span>Recent Activity</span>
|
||||
<span>Sorted by date</span>
|
||||
</div>
|
||||
<div class="timeline" id="recent-activity-list" data-activity-feed data-batch-size="6">
|
||||
{% set entries = [] %}
|
||||
{% for section_id in ['projects', 'articles', 'blog'] %}
|
||||
{% for item in site.get('/' ~ section_id).children %}
|
||||
{% if entries.append(item) %}{% endif %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% for project in site.query('/projects') %}
|
||||
{% if project._model == 'project' %}
|
||||
{% set devlog = site.get(project.path ~ '/devlog') %}
|
||||
{% if devlog %}
|
||||
{% for entry in devlog.children %}
|
||||
{% if entries.append(entry) %}{% endif %}
|
||||
<div class="homepage-discovery-grid">
|
||||
<section class="homepage-panel" aria-labelledby="recent-activity-heading">
|
||||
<div class="section-heading">
|
||||
<span id="recent-activity-heading">Recent Activity</span>
|
||||
<span>Sorted by date</span>
|
||||
</div>
|
||||
<div class="timeline" id="recent-activity-list" data-activity-feed data-batch-size="6">
|
||||
{% set entries = [] %}
|
||||
{% for section_id in ['articles', 'blog'] %}
|
||||
{% for item in site.get('/' ~ section_id).children %}
|
||||
{% if entries.append(item) %}{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% for item in entries|sort(attribute='date', reverse=true) %}
|
||||
<article class="timeline-entry accent-{{ item.kicker|lower }}" data-activity-item>
|
||||
<div class="entry-meta">
|
||||
<span class="label">{{ item.kicker }}</span>
|
||||
<time datetime="{{ item.date }}">{{ item.date|dateformat('YYYY.MM.dd') }}</time>
|
||||
</div>
|
||||
<h2><a href="{{ item|url }}">{{ item.title }}</a></h2>
|
||||
<p>{{ item.summary }}</p>
|
||||
<a class="read-more" href="{{ item|url }}">Read entry →</a>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="activity-controls" data-activity-controls hidden>
|
||||
<p class="activity-status" data-activity-status aria-live="polite"></p>
|
||||
<button class="activity-more-button" type="button" data-activity-more aria-controls="recent-activity-list">Show 6 more</button>
|
||||
{% endfor %}
|
||||
{% for project in site.query('/projects') %}
|
||||
{% if project._model == 'project' %}
|
||||
{% set devlog = site.get(project.path ~ '/devlog') %}
|
||||
{% if devlog %}
|
||||
{% for entry in devlog.children %}
|
||||
{% if entries.append(entry) %}{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% for item in entries|sort(attribute='date', reverse=true) %}
|
||||
<article class="timeline-entry accent-{{ item.kicker|lower }}" data-activity-item>
|
||||
<div class="entry-meta">
|
||||
<span class="label">{{ item.kicker }}</span>
|
||||
<time datetime="{{ item.date }}">{{ item.date|dateformat('YYYY.MM.dd') }}</time>
|
||||
</div>
|
||||
<h2><a href="{{ item|url }}">{{ item.title }}</a></h2>
|
||||
<p>{{ item.summary }}</p>
|
||||
<a class="read-more" href="{{ item|url }}">Read entry →</a>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="activity-controls" data-activity-controls hidden>
|
||||
<p class="activity-status" data-activity-status aria-live="polite"></p>
|
||||
<button class="activity-more-button" type="button" data-activity-more aria-controls="recent-activity-list">Show 6 more</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="homepage-panel featured-projects-panel" aria-labelledby="featured-projects-heading">
|
||||
<div class="section-heading">
|
||||
<span id="featured-projects-heading">Featured Projects</span>
|
||||
<span>Curated work</span>
|
||||
</div>
|
||||
<div class="featured-project-list">
|
||||
{% for project in site.query('/projects').filter(F._model == 'project').filter(F.featured == true).order_by('featured_order', '-date').limit(3) %}
|
||||
<article class="featured-project">
|
||||
<div class="entry-meta">
|
||||
<span class="label">{{ project.status }}</span>
|
||||
<time datetime="{{ project.date }}">{{ project.date|dateformat('YYYY.MM.dd') }}</time>
|
||||
</div>
|
||||
<h2><a href="{{ project|url }}">{{ project.title }}</a></h2>
|
||||
<p>{{ project.summary }}</p>
|
||||
<a class="read-more" href="{{ project|url }}">View project →</a>
|
||||
</article>
|
||||
{% else %}
|
||||
<p class="featured-projects-empty">Selected projects will appear here.</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<a class="featured-projects-link" href="{{ '/projects'|url }}">View all projects →</a>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -14,6 +14,7 @@ from project_sources import ( # noqa: E402
|
||||
RepositoryNotPublicError,
|
||||
append_fields,
|
||||
fetch_json,
|
||||
load_registry,
|
||||
metadata_digest,
|
||||
normalize_tag_value,
|
||||
parse_record,
|
||||
@@ -25,6 +26,10 @@ from project_sources import ( # noqa: E402
|
||||
|
||||
|
||||
class ProjectSourceTests(unittest.TestCase):
|
||||
def test_registry_feature_order_is_optional_and_numeric(self) -> None:
|
||||
site_root = Path(__file__).resolve().parents[1]
|
||||
self.assertEqual(load_registry(site_root)[0]["featured_order"], 10)
|
||||
|
||||
def test_registry_urls_must_be_public_https_without_credentials(self) -> None:
|
||||
self.assertEqual(
|
||||
validate_url("https://git.example.test/owner/repo.git", "repository"),
|
||||
|
||||
Reference in New Issue
Block a user