diff --git a/assets/static/style.css b/assets/static/style.css index b4e2e88..a2093d3 100644 --- a/assets/static/style.css +++ b/assets/static/style.css @@ -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; } diff --git a/configs/project-sources.ini b/configs/project-sources.ini index cde519a..157e6ba 100644 --- a/configs/project-sources.ini +++ b/configs/project-sources.ini @@ -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 diff --git a/models/project.ini b/models/project.ini index e563b38..e893ac5 100644 --- a/models/project.ini +++ b/models/project.ini @@ -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 diff --git a/scripts/project_sources.py b/scripts/project_sources.py index 6366595..6186901 100644 --- a/scripts/project_sources.py +++ b/scripts/project_sources.py @@ -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") diff --git a/templates/base.html b/templates/base.html index 60ffa61..931b5b0 100644 --- a/templates/base.html +++ b/templates/base.html @@ -9,7 +9,7 @@ - + diff --git a/templates/home.html b/templates/home.html index c788295..660af69 100644 --- a/templates/home.html +++ b/templates/home.html @@ -8,42 +8,69 @@
-
- Recent Activity - Sorted by date -
-
- {% 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 %} +
+
+
+ Recent Activity + Sorted by date +
+
+ {% 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) %} - - {% endfor %} -
- + +
+ +
diff --git a/tests/test_project_sources.py b/tests/test_project_sources.py index 23e2c7d..48b0c3e 100644 --- a/tests/test_project_sources.py +++ b/tests/test_project_sources.py @@ -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"),