my website at ewancroft.uk
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

add universal Blog Archive header

- Remove StatsDisplay.svelte
- Create ArchiveHeader.svelte

Ewan Croft d821c654 1f582f6a

+225 -35
+216
src/lib/components/archive/ArchiveHeader.svelte
··· 1 + <script lang="ts"> 2 + import { fly } from "svelte/transition"; 3 + import { quintOut } from "svelte/easing"; 4 + import { formatNumber } from "$utils/formatters"; 5 + import { calculateTotalReadTime, calculateTotalWordCount, formatReadTime } from "$utils/tally"; 6 + import DocumentIcon from "$components/icons/utility/DocumentIcon.svelte"; 7 + 8 + export let groupedByYear: any[]; 9 + 10 + // Calculate total posts across all years 11 + $: totalPosts = groupedByYear.reduce((total, yearGroup) => { 12 + return total + Object.values(yearGroup.months).reduce((yearTotal: number, postsInMonth) => { 13 + return yearTotal + (postsInMonth as any[]).length; 14 + }, 0); 15 + }, 0); 16 + 17 + // Calculate total stats across all posts 18 + $: allPosts = groupedByYear.flatMap(yearGroup => 19 + Object.values(yearGroup.months).flatMap((postsInMonth) => postsInMonth as any[]) 20 + ); 21 + 22 + $: rawTotalReadTime = calculateTotalReadTime(allPosts); 23 + $: totalReadTime = formatReadTime(rawTotalReadTime); 24 + $: totalWordCount = calculateTotalWordCount(allPosts); 25 + 26 + // Labels for singular/plural 27 + $: postLabel = totalPosts === 1 ? "post" : "posts"; 28 + $: wordLabel = totalWordCount === 1 ? "word" : "words"; 29 + </script> 30 + 31 + <header 32 + class="archive-header" 33 + in:fly={{ y: -20, duration: 400, delay: 0, easing: quintOut }} 34 + > 35 + <div class="header-content"> 36 + <div class="title-section"> 37 + <div class="icon-wrapper"> 38 + <DocumentIcon size="24" /> 39 + </div> 40 + <h1 class="archive-title">Blog Archive</h1> 41 + </div> 42 + 43 + <div class="stats-section"> 44 + <div class="primary-stat"> 45 + <span class="stat-number">{formatNumber(totalPosts)}</span> 46 + <span class="stat-label">{postLabel}</span> 47 + </div> 48 + 49 + <div class="divider">•</div> 50 + 51 + <div class="secondary-stats"> 52 + <span class="stat-item"> 53 + <span class="stat-value">{totalReadTime}</span> 54 + <span class="stat-sub-label">read time</span> 55 + </span> 56 + <span class="stat-item"> 57 + <span class="stat-value">{formatNumber(totalWordCount)}</span> 58 + <span class="stat-sub-label">{wordLabel}</span> 59 + </span> 60 + </div> 61 + </div> 62 + </div> 63 + </header> 64 + 65 + <style> 66 + .archive-header { 67 + margin-bottom: 16px; 68 + padding: 0 16px; 69 + } 70 + 71 + .header-content { 72 + display: flex; 73 + align-items: center; 74 + justify-content: space-between; 75 + gap: 24px; 76 + padding-bottom: 16px; 77 + } 78 + 79 + .title-section { 80 + display: flex; 81 + align-items: center; 82 + gap: 12px; 83 + } 84 + 85 + .icon-wrapper { 86 + display: flex; 87 + align-items: center; 88 + justify-content: center; 89 + width: 48px; 90 + height: 48px; 91 + background: var(--button-bg); 92 + border-radius: 12px; 93 + color: var(--text-color); 94 + transition: all 0.3s ease; 95 + } 96 + 97 + .archive-title { 98 + font-size: 2rem; 99 + font-weight: 700; 100 + color: var(--link-color); 101 + margin: 0; 102 + letter-spacing: -0.02em; 103 + } 104 + 105 + .stats-section { 106 + display: flex; 107 + align-items: center; 108 + gap: 16px; 109 + font-size: 0.95rem; 110 + } 111 + 112 + .primary-stat { 113 + display: flex; 114 + align-items: baseline; 115 + gap: 6px; 116 + } 117 + 118 + .stat-number { 119 + font-size: 1.5rem; 120 + font-weight: 700; 121 + color: var(--link-color); 122 + line-height: 1; 123 + } 124 + 125 + .stat-label { 126 + font-weight: 500; 127 + color: var(--text-color); 128 + opacity: 0.8; 129 + } 130 + 131 + .divider { 132 + color: var(--text-color); 133 + opacity: 0.4; 134 + font-weight: bold; 135 + } 136 + 137 + .secondary-stats { 138 + display: flex; 139 + flex-direction: column; 140 + gap: 2px; 141 + font-size: 0.85rem; 142 + } 143 + 144 + .stat-item { 145 + display: flex; 146 + align-items: baseline; 147 + gap: 4px; 148 + } 149 + 150 + .stat-value { 151 + font-weight: 600; 152 + color: var(--text-color); 153 + } 154 + 155 + .stat-sub-label { 156 + font-weight: 400; 157 + color: var(--text-color); 158 + opacity: 0.7; 159 + } 160 + 161 + /* Responsive adjustments */ 162 + @media (max-width: 768px) { 163 + .header-content { 164 + flex-direction: column; 165 + align-items: flex-start; 166 + gap: 16px; 167 + } 168 + 169 + .archive-title { 170 + font-size: 1.75rem; 171 + } 172 + 173 + .stats-section { 174 + align-self: stretch; 175 + justify-content: space-between; 176 + } 177 + 178 + .secondary-stats { 179 + flex-direction: row; 180 + gap: 12px; 181 + } 182 + 183 + .stat-item { 184 + flex-direction: column; 185 + gap: 0; 186 + text-align: right; 187 + } 188 + } 189 + 190 + @media (max-width: 640px) { 191 + .archive-header { 192 + padding: 0 8px; 193 + } 194 + 195 + .title-section { 196 + gap: 8px; 197 + } 198 + 199 + .icon-wrapper { 200 + width: 40px; 201 + height: 40px; 202 + } 203 + 204 + .archive-title { 205 + font-size: 1.5rem; 206 + } 207 + 208 + .stat-number { 209 + font-size: 1.25rem; 210 + } 211 + 212 + .secondary-stats { 213 + font-size: 0.8rem; 214 + } 215 + } 216 + </style>
+3 -6
src/lib/components/archive/MonthSection.svelte
··· 1 1 <script lang="ts"> 2 2 import { slide } from "svelte/transition"; 3 - 4 3 import { quintOut } from "svelte/easing"; 5 4 import { ArchiveCard } from "./index"; 6 - import StatsDisplay from "./StatsDisplay.svelte"; 7 5 8 6 export let monthName: string; 9 7 export let postsInMonth: any[]; ··· 23 21 class="mb-12 ml-4" 24 22 in:slide={{ delay: 100 + monthIndex * 50, duration: 300, easing: quintOut }} 25 23 > 26 - <h2 class="text-2xl font-bold mb-1 ml-2">{monthName}</h2> 27 - <StatsDisplay {totalReadTime} {totalWordCount} {postCount} /> 24 + <h2 class="text-3xl font-bold mb-1">{monthName}</h2> 28 25 <div 29 - class="grid grid-cols-[repeat(auto-fill,minmax(280px,1fr)_)] gap-x-6 gap-y-6 mx-4 my-8" 26 + class="grid grid-cols-[repeat(auto-fill,minmax(280px,1fr)_)] gap-x-6 gap-y-6 my-8" 30 27 > 31 28 {#each postsInMonth as post, postIndex (post.rkey)} 32 29 <ArchiveCard type="post" {post} {monthIndex} {postIndex} {localeLoaded} postNumber={post.postNumber} /> 33 30 {/each} 34 31 </div> 35 - </div> 32 + </div>
-24
src/lib/components/archive/StatsDisplay.svelte
··· 1 - <script lang="ts"> 2 - import { formatNumber } from "$utils/formatters"; 3 - 4 - export let totalReadTime: string; 5 - export let totalWordCount: number; 6 - export let postCount: number | undefined = undefined; 7 - 8 - // Determine singular or plural for word count 9 - $: wordLabel = totalWordCount === 1 ? "word" : "words"; 10 - $: postLabel = postCount === 1 ? "post" : "posts"; 11 - </script> 12 - 13 - {#if postCount !== undefined} 14 - <p class="text-sm opacity-50 mb-4 ml-2"> 15 - {totalReadTime} read time • {formatNumber(totalWordCount)} 16 - {wordLabel} • {formatNumber(postCount)} 17 - {postLabel} 18 - </p> 19 - {:else} 20 - <div class="mb-6 ml-4 text-sm opacity-70"> 21 - <p>Total Read Time: {totalReadTime}</p> 22 - <p>Total Word Count: {formatNumber(totalWordCount)} {wordLabel}</p> 23 - </div> 24 - {/if}
-2
src/lib/components/archive/YearContent.svelte
··· 3 3 import { quintOut } from "svelte/easing"; 4 4 import MonthSection from "./MonthSection.svelte"; 5 5 import { calculateTotalReadTime, calculateTotalWordCount, formatReadTime } from "$utils/tally"; 6 - import StatsDisplay from "./StatsDisplay.svelte"; 7 6 8 7 export const year: number = 0; 9 8 export let months: Record<string, any[]>; ··· 25 24 out:fade={{ duration: 200 }} 26 25 class="year-content" 27 26 > 28 - <StatsDisplay totalReadTime={yearlyTotalReadTime} totalWordCount={yearlyTotalWordCount} /> 29 27 30 28 {#each Object.entries(months) as [monthName, postsInMonth], monthIndex} 31 29 <MonthSection
+1 -1
src/lib/components/archive/index.ts
··· 2 2 export { default as YearContent } from "./YearContent.svelte"; 3 3 export { default as MonthSection } from "./MonthSection.svelte"; 4 4 export { default as ArchiveCard } from "./ArchiveCard.svelte"; 5 - export { default as StatsDisplay } from "./StatsDisplay.svelte"; 5 + export { default as ArchiveHeader } from "./ArchiveHeader.svelte";
+5 -2
src/routes/blog/+page.svelte
··· 1 1 <script lang="ts"> 2 2 import { onMount } from "svelte"; 3 - import YearTabs from "$components/archive/YearTabs.svelte"; 4 - import YearContent from "$components/archive/YearContent.svelte"; 3 + import { YearContent, YearTabs, ArchiveHeader } from "$components/archive"; 5 4 import { getStores } from "$app/stores"; 6 5 const { page } = getStores(); 7 6 const { data } = $props(); ··· 184 183 <p class="mt-2 text-sm">Posts were found but none have valid content, titles, and dates.</p> 185 184 </div> 186 185 {:else} 186 + 187 + <!-- Archive header with stats --> 188 + <ArchiveHeader {groupedByYear} /> 189 + 187 190 <!-- Year tabs with animated indicator --> 188 191 <YearTabs {groupedByYear} bind:activeYear /> 189 192