Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- uses: actions/setup-node@v4
with:
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/verify_build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- uses: actions/setup-node@v4
with:
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [Unreleased]

### Fixed — Project "Last edited" dates on the deployed site
- GitHub Actions workflows (`deploy.yml`, `verify_build.yml`) now check out the full git history (`fetch-depth: 0`) so the `projectDates` data loader can read the per-project last-commit timestamp. Previously the shallow clone caused every project card to show the same date.

### Added — Sortable project cards on the home page
- `HomeFeatures.vue` now renders a sort control with four options: `Last edited ↓ / ↑` and `Alphabetically ↓ / ↑`. Default sort is newest-first by last edited; projects without a known timestamp sort last.

### Added — Shared mock banner
- New shared mock banner under `docs/public/design-system/v1/mock-banner.{css,js}` — auto-injects a "this is a mock" strip across the top of every prototype, with optional `data-banner-text` override
- CI check `npm run lint:mocks` (wired into `.github/workflows/verify_build.yml`) fails the build if a prototype HTML file under `docs/public/projects/` doesn't reference the shared banner; allowlist supported for deliberate exceptions (currently `deltag-aarhus`)
Expand Down
98 changes: 93 additions & 5 deletions docs/.vitepress/theme/HomeFeatures.vue
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
<script setup>
import { computed } from 'vue'
import { computed, ref } from 'vue'
import { useData, withBase } from 'vitepress'
import { data as projectDates } from './projectDates.data.js'

const { frontmatter } = useData()

const sortOptions = [
{ value: 'edited-desc', label: 'Last edited ↓' },
{ value: 'edited-asc', label: 'Last edited ↑' },
{ value: 'alpha-asc', label: 'Alphabetically ↓' },
{ value: 'alpha-desc', label: 'Alphabetically ↑' },
]

const sort = ref('edited-desc')

function formatDate(isoString) {
if (!isoString) return null
return new Date(isoString).toLocaleString('da-DK', {
Expand All @@ -17,11 +26,48 @@ function formatDate(isoString) {
})
}

function sortKey(title) {
// Strip HTML tags and any leading non-letter characters (emoji, punctuation,
// whitespace) so decorative prefixes like "🔒 " don't affect ordering.
return (title || '')
.replace(/<[^>]*>/g, '')
.replace(/^[^\p{L}]+/u, '')
}

const features = computed(() => {
return (frontmatter.value.features || []).map(f => ({
...f,
lastEdited: formatDate(projectDates[f.link]),
}))
const items = (frontmatter.value.features || []).map(f => {
const iso = projectDates[f.link] || null
return {
...f,
lastEditedIso: iso,
lastEdited: formatDate(iso),
}
})

const sorted = [...items]
switch (sort.value) {
case 'edited-desc':
case 'edited-asc': {
const dir = sort.value === 'edited-desc' ? -1 : 1
sorted.sort((a, b) => {
// Items without a timestamp always sort last.
if (!a.lastEditedIso && !b.lastEditedIso) return 0
if (!a.lastEditedIso) return 1
if (!b.lastEditedIso) return -1
return dir * a.lastEditedIso.localeCompare(b.lastEditedIso)
})
break
}
case 'alpha-asc':
case 'alpha-desc': {
const dir = sort.value === 'alpha-asc' ? 1 : -1
sorted.sort((a, b) =>
dir * sortKey(a.title).localeCompare(sortKey(b.title), 'da')
)
break
}
}
return sorted
})

const grid = computed(() => {
Expand All @@ -37,6 +83,14 @@ const grid = computed(() => {
<template>
<div v-if="features.length" class="VPFeatures custom-features">
<div class="container">
<div class="sort-bar">
<label class="sort-label" for="features-sort">Sort by</label>
<select id="features-sort" v-model="sort" class="sort-select">
<option v-for="opt in sortOptions" :key="opt.value" :value="opt.value">
{{ opt.label }}
</option>
</select>
</div>
<div class="items">
<div
v-for="feature in features"
Expand Down Expand Up @@ -90,6 +144,40 @@ const grid = computed(() => {
max-width: 1152px;
}

.sort-bar {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
padding: 0 8px 12px;
}

.sort-label {
font-size: 13px;
color: var(--vp-c-text-2);
}

.sort-select {
appearance: none;
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
color: var(--vp-c-text-1);
font-size: 13px;
padding: 6px 28px 6px 10px;
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'><path fill='gray' d='M4 6l4 4 4-4z'/></svg>");
background-repeat: no-repeat;
background-position: right 8px center;
cursor: pointer;
transition: border-color 0.25s;
}

.sort-select:hover,
.sort-select:focus {
border-color: var(--vp-c-brand-1);
outline: none;
}

.items {
display: flex;
flex-wrap: wrap;
Expand Down
Loading