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
29 changes: 3 additions & 26 deletions app/components/card/CaseStudy.vue
Original file line number Diff line number Diff line change
Expand Up @@ -37,29 +37,6 @@ interface CaseStudy {

const props = defineProps<{ study: CaseStudy }>()

const outcomeVariant: Record<CaseStudy['outcome'], 'success' | 'default' | 'error' | 'warning'> = {
success: 'success',
partial: 'default',
failed: 'error',
inconclusive: 'default',
ongoing: 'warning',
}
const outcomeLabel: Record<CaseStudy['outcome'], string> = {
success: 'Success',
partial: 'Partial',
failed: 'Failed',
inconclusive: 'Inconclusive',
ongoing: 'Ongoing',
}

const scaleLabel: Record<string, string> = {
neighborhood: 'Neighborhood',
city: 'City',
region: 'Region',
national: 'National',
global: 'Global',
}

function yearOf(s?: string | null): string | null {
if (!s) return null
const m = /^(\d{4})/.exec(s)
Expand Down Expand Up @@ -130,8 +107,8 @@ const sourceCount = computed(() => props.study.sources?.length ?? 0)
title="Verified"
/>
</NuxtLink>
<UiBadge class="shrink-0" :variant="outcomeVariant[study.outcome]">
{{ outcomeLabel[study.outcome] }}
<UiBadge class="shrink-0" :variant="outcomeBadgeVariant(study.outcome)">
{{ outcomeBadgeLabel(study.outcome) }}
</UiBadge>
</div>
<!-- Meta line: implementer · dates · scale, single greyscale row -->
Expand All @@ -157,7 +134,7 @@ const sourceCount = computed(() => props.study.sources?.length ?? 0)
</span>
<span v-if="study.scale" class="inline-flex items-center gap-1.5">
<UIcon class="size-3.5 text-gray-400" name="lucide:globe" />
{{ scaleLabel[study.scale] ?? study.scale }}
{{ scaleBadgeLabel(study.scale) }}
</span>
</div>
<UiMarkdown
Expand Down
40 changes: 40 additions & 0 deletions app/components/caseStudy/LinkCard.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<script setup lang="ts">
import { safeUrl } from '~/utils/safeUrl'
// A labeled card listing external links ({ url, title }) — the Sources and
// Links sections of a case study render identically apart from icon + label, so
// they share this. Renders nothing when there are no items.
defineProps<{
icon: string
label: string
items: { url: string; title?: string | null }[]
}>()
</script>

<template>
<div v-if="items.length" class="rounded-2xl border border-gray-200 bg-gray-50 overflow-hidden">
<div class="flex items-center gap-2 p-4 sm:p-6 border-b border-gray-200">
<UIcon class="size-4 text-gray-400" :name="icon" />
<p class="text-xs font-mono uppercase tracking-wide text-gray-400">
{{ label }}
</p>
<span class="text-xs font-mono text-gray-400">
{{ items.length }}
</span>
</div>
<ul class="divide-y divide-gray-200 bg-white">
<li v-for="(item, i) in items" :key="i">
<a
class="flex items-center gap-3 px-4 py-2.5 sm:px-6 hover:bg-gray-50 transition-colors min-w-0"
rel="nofollow noopener noreferrer"
target="_blank"
:href="safeUrl(item.url)"
>
<UIcon class="size-3.5 text-gray-400 shrink-0" name="lucide:external-link" />
<span class="truncate text-sm text-primary-700">
{{ item.title || item.url }}
</span>
</a>
</li>
</ul>
</div>
</template>
22 changes: 2 additions & 20 deletions app/components/issue/TreeNode.vue
Original file line number Diff line number Diff line change
Expand Up @@ -44,21 +44,6 @@ const nodeIconClass = computed(() => {
if (isSolution.value) return 'text-primary-600'
return 'text-gray-400'
})

const outcomeVariant: Record<string, 'success' | 'default' | 'error' | 'warning'> = {
success: 'success',
partial: 'default',
failed: 'error',
inconclusive: 'default',
ongoing: 'warning',
}
const outcomeLabel: Record<string, string> = {
success: 'Success',
partial: 'Partial',
failed: 'Failed',
inconclusive: 'Inconclusive',
ongoing: 'Ongoing',
}
</script>

<template>
Expand Down Expand Up @@ -90,11 +75,8 @@ const outcomeLabel: Record<string, string> = {
<UiBadge v-else-if="node.solutionStatus === 'done'" variant="success">
Done
</UiBadge>
<UiBadge
v-if="isCaseStudy && node.outcome"
:variant="outcomeVariant[node.outcome] ?? 'default'"
>
{{ outcomeLabel[node.outcome] ?? node.outcome }}
<UiBadge v-if="isCaseStudy && node.outcome" :variant="outcomeBadgeVariant(node.outcome)">
{{ outcomeBadgeLabel(node.outcome) }}
</UiBadge>
<div
v-if="!isCaseStudy"
Expand Down
15 changes: 15 additions & 0 deletions app/components/node/BackLink.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<script setup lang="ts">
// Small "← back" link used at the top of a node's meta pages (Contributors /
// History) where there's no tab bar to navigate back with.
defineProps<{ to: string; label?: string }>()
</script>

<template>
<NuxtLink
class="inline-flex items-center gap-1.5 text-xs font-mono text-gray-400 hover:text-gray-600 transition-colors"
:to="to"
>
<UIcon class="size-3.5" name="lucide:arrow-left" />
{{ label ?? 'Back' }}
</NuxtLink>
</template>
41 changes: 41 additions & 0 deletions app/components/node/MetaLinks.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<script setup lang="ts">
// Quiet "Contributors · History" links at the very bottom of a node overview.
// Deliberately low-key: useful, but not what the page is about. Shared by the
// issue/solution overview and the case-study overview.
const props = defineProps<{
// Route prefix for the node, e.g. `/issue/42` or `/case-study/7`.
base: string
// Optional node kind, forwarded as analytics context.
kind?: string
}>()

const { track } = useUmami()

function onClick(tab: 'contributors' | 'history') {
track('Overview meta link', props.kind ? { tab, kind: props.kind } : { tab })
}
</script>

<template>
<div class="flex items-center justify-center gap-4 pt-1 text-xs font-mono text-gray-400">
<NuxtLink
class="inline-flex items-center gap-1.5 hover:text-gray-600 transition-colors"
:to="`${base}/contributors`"
@click="onClick('contributors')"
>
<UIcon class="size-3.5" name="lucide:users" />
Contributors
</NuxtLink>
<span class="text-gray-300">
·
</span>
<NuxtLink
class="inline-flex items-center gap-1.5 hover:text-gray-600 transition-colors"
:to="`${base}/history`"
@click="onClick('history')"
>
<UIcon class="size-3.5" name="lucide:history" />
History
</NuxtLink>
</div>
</template>
Loading