Non-official site for The Life Series Minecraft hardcore survival multiplayer series housing every video www.life-series.online
0
fork

Configure Feed

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

Home page (#61)

* Home page

* Seasons pages

* Better scroll to active link

* Fix session link horizontal overflow

* Season link interaction

authored by

Ghustvn and committed by
GitHub
90d4e58a a1fe85c4

+402 -97
src/assets/rewrite/images/og/index.webp

This is a binary file and will not be displayed.

src/assets/rewrite/images/seasons/3rd-life.webp

This is a binary file and will not be displayed.

src/assets/rewrite/images/seasons/double-life.webp

This is a binary file and will not be displayed.

src/assets/rewrite/images/seasons/last-life.webp

This is a binary file and will not be displayed.

src/assets/rewrite/images/seasons/limited-life.webp

This is a binary file and will not be displayed.

src/assets/rewrite/images/seasons/nice-life.webp

This is a binary file and will not be displayed.

src/assets/rewrite/images/seasons/past-life.webp

This is a binary file and will not be displayed.

src/assets/rewrite/images/seasons/secret-life.webp

This is a binary file and will not be displayed.

src/assets/rewrite/images/seasons/wild-life.webp

This is a binary file and will not be displayed.

+106
src/components/rewrite/MemberList.astro
··· 1 + --- 2 + import type { CollectionEntry } from 'astro:content'; 3 + import MemberItem from '@/components/rewrite/MemberItem.astro'; 4 + import { isCurrentPage } from '@/utils/url'; 5 + 6 + interface Props { 7 + members: CollectionEntry<'membersRewrite'>[]; 8 + season: CollectionEntry<'seasonsRewrite'>; 9 + } 10 + 11 + const { members, season } = Astro.props; 12 + --- 13 + 14 + <ul role="list" aria-labelledby="sidebar-title" class="member-list"> 15 + { 16 + members.map((member) => { 17 + const isPlaying = Object.keys(season.data.videos).includes(member.data.name); 18 + 19 + return ( 20 + <li> 21 + <a 22 + class="member-item-link" 23 + role={!isPlaying ? 'link' : undefined} 24 + aria-disabled={!isPlaying ? 'true' : undefined} 25 + href={isPlaying ? `/rewrite/seasons/${season.id}/${member.data.name}` : undefined} 26 + aria-current={isPlaying ? isCurrentPage(Astro.url.pathname, `/rewrite/seasons/${season.id}/${member.data.name}`) : undefined} 27 + > 28 + <MemberItem memberName={member.data.name} /> 29 + </a> 30 + </li> 31 + ); 32 + }) 33 + } 34 + </ul> 35 + 36 + <style> 37 + .member-list { 38 + min-width: 0; 39 + 40 + &:has(.member-item-link:focus-visible) .member-item-link:not(:focus-visible) { 41 + filter: grayscale(75%); 42 + opacity: 0.8; 43 + } 44 + 45 + @media (width < 48rem) { 46 + padding-inline-end: 12px; 47 + } 48 + 49 + @media (hover) { 50 + &:has(.member-item-link:not([aria-disabled='true']):hover) 51 + .member-item-link:not(:hover) { 52 + filter: grayscale(75%); 53 + opacity: 0.8; 54 + } 55 + } 56 + } 57 + 58 + .member-item-link { 59 + display: grid; /* Fixes parent to be 1px taller */ 60 + justify-content: start; 61 + inline-size: 100%; 62 + padding-block: 4px; 63 + text-decoration: none; 64 + transition-property: filter, opacity; 65 + transition-duration: 100ms; 66 + transition-timing-function: linear; 67 + 68 + &:focus-visible { 69 + outline: none; 70 + 71 + &:not([aria-current='page']) { 72 + translate: 8px; 73 + } 74 + 75 + > div { 76 + outline: 2px solid var(--color-canvas-text); 77 + outline-offset: 4px; 78 + } 79 + } 80 + 81 + @media (prefers-reduced-motion: no-preference) { 82 + transition: 83 + filter 100ms linear, 84 + opacity 100ms linear, 85 + translate var(--spring-transition-duration) var(--spring-transition-timing-function); 86 + } 87 + 88 + @media (hover) { 89 + &:hover:not([aria-current='page']):not([aria-disabled]) { 90 + translate: 8px; 91 + } 92 + } 93 + 94 + &[aria-current='page'] { 95 + translate: 12px; 96 + text-decoration-line: underline; 97 + text-decoration-style: wavy; 98 + font-weight: var(--font-weight-heading); 99 + } 100 + 101 + &[aria-disabled='true'] { 102 + filter: grayscale(75%); 103 + text-decoration: line-through 2px; 104 + } 105 + } 106 + </style>
+4
src/components/rewrite/SeasonLayout.astro
··· 80 80 } 81 81 82 82 .sidebar-nav-list { 83 + :global(.js) & { 84 + visibility: hidden; 85 + } 86 + 83 87 @media (width < 48rem) { 84 88 display: grid; 85 89 border-block: 1px solid var(--color-surface-2);
+1 -2
src/layouts/rewrite/BaseLayout.astro
··· 2 2 import { Font } from 'astro:assets'; 3 3 import { Head } from 'astro-capo'; 4 4 import CustomCursor from '@/components/rewrite/CustomCursor.astro'; 5 - import SkipLink from '@/components/rewrite/SkipLink.astro'; 6 5 import '@/styles/rewrite/global.css'; 7 6 8 7 const { title } = Astro.props; ··· 10 9 11 10 <html lang="en"> 12 11 <Head> 12 + <script is:inline>document.documentElement.classList.add('js')</script> 13 13 <meta charset="utf-8" /> 14 14 <meta name="theme-color" content="#d9ebd2" /> 15 15 <link rel="icon" href="/rewrite/favicon.png" /> ··· 24 24 <!-- <PostHog /> --> 25 25 </Head> 26 26 <body> 27 - <SkipLink /> 28 27 <slot /> 29 28 <CustomCursor /> 30 29 </body>
+153
src/pages/rewrite/index.astro
··· 1 + --- 2 + import { Image } from 'astro:assets'; 3 + import type { CollectionEntry } from 'astro:content'; 4 + import { getCollection } from 'astro:content'; 5 + import { SEO } from 'astro-seo'; 6 + import ogIndexImage from '@/assets/rewrite/images/og/index.webp'; 7 + import BaseLayout from '@/layouts/rewrite/BaseLayout.astro'; 8 + import { getSeasonImage } from '@/utils/rewrite/images'; 9 + 10 + const { isRewrite } = Astro.locals; 11 + if (!isRewrite) { 12 + return Astro.redirect('/404'); 13 + } 14 + 15 + const seasons = await getCollection('seasonsRewrite'); 16 + 17 + const ogTitle = 'The Life Series Minecraft hardcore survival multiplayer series.'; 18 + const ogDescription = 'Non-official site. Watch all The Life Series member point of views from all seasons.'; 19 + const ogImage = ogIndexImage.src; 20 + const ogImageAlt = 'Grid of 8 Life Series season logos in order from newest to oldest - "Nice Life", "Past Life", "Wild Life", "Secret Life", "Limited Life", "Double Life", "Last Life", "Third Life"'; 21 + 22 + function getUrl(season: CollectionEntry<'seasonsRewrite'>) { 23 + if (season.data.sessionCount) { 24 + return `/rewrite/seasons/${season.id}/sessions/1`; 25 + } 26 + 27 + return `/rewrite/seasons/${season.id}`; 28 + } 29 + --- 30 + 31 + <BaseLayout title="Home"> 32 + <SEO 33 + slot="seo" 34 + title={ogTitle} 35 + description={ogDescription} 36 + openGraph={{ 37 + basic: { 38 + title: ogTitle, 39 + type: 'website', 40 + image: ogImage, 41 + }, 42 + image: { 43 + alt: ogImageAlt, 44 + }, 45 + optional: { 46 + description: ogDescription, 47 + siteName: 'The Life Series', 48 + }, 49 + }} 50 + twitter={{ 51 + card: 'summary', 52 + creator: '@ghustvn', 53 + title: ogTitle, 54 + description: ogDescription, 55 + image: ogImage, 56 + imageAlt: ogImageAlt, 57 + }} 58 + /> 59 + <main> 60 + <h1 class="visually-hidden">Home - Life Series - Non-official site</h1> 61 + <nav> 62 + <ol role="list" reversed> 63 + { 64 + seasons.map((season) => ( 65 + <li> 66 + <a href={getUrl(season)}> 67 + <Image 68 + src={getSeasonImage(season.id)} 69 + width="360" 70 + alt={season.data.title} 71 + loading="eager" 72 + /> 73 + </a> 74 + </li> 75 + )) 76 + } 77 + </ol> 78 + </nav> 79 + </main> 80 + </BaseLayout> 81 + 82 + <style> 83 + main { 84 + box-sizing: border-box; 85 + display: grid; 86 + min-block-size: 100svh; 87 + place-items: center; 88 + padding: 32px; 89 + } 90 + 91 + nav { 92 + inline-size: 100%; 93 + max-inline-size: 636px; 94 + } 95 + 96 + ol { 97 + display: grid; 98 + grid-template-columns: repeat(auto-fit, minmax(min(180px, 100%), 1fr)); 99 + place-items: center; 100 + gap: 48px; 101 + } 102 + 103 + li { 104 + @media (prefers-reduced-motion: no-preference) { 105 + &:nth-child(odd) { 106 + --angle: 2deg; 107 + } 108 + &:nth-child(even) { 109 + --angle: -2deg; 110 + } 111 + } 112 + } 113 + 114 + a { 115 + @media (prefers-reduced-motion: no-preference) { 116 + transition: 117 + scale var(--spring-transition-duration), 118 + rotate var(--spring-transition-duration); 119 + transition-timing-function: var(--spring-transition-timing-function); 120 + 121 + &:focus-visible { 122 + scale: 1.075; 123 + rotate: var(--angle); 124 + } 125 + } 126 + 127 + @media (hover) and (prefers-reduced-motion: no-preference) { 128 + &:hover { 129 + scale: 1.075; 130 + rotate: var(--angle); 131 + } 132 + } 133 + } 134 + 135 + img { 136 + max-inline-size: 180px; 137 + transition-property: filter, opacity; 138 + transition-duration: 100ms; 139 + transition-timing-function: linear; 140 + 141 + :has(a:focus-visible) a:not(:focus-visible) & { 142 + filter: grayscale(75%); 143 + opacity: 0.8; 144 + } 145 + 146 + @media (hover) { 147 + :has(a:hover) a:not(:hover) & { 148 + filter: grayscale(75%); 149 + opacity: 0.8; 150 + } 151 + } 152 + } 153 + </style>
+75
src/pages/rewrite/seasons/[season_id].astro
··· 1 + --- 2 + import { getCollection } from 'astro:content'; 3 + import { SEO } from 'astro-seo'; 4 + import MemberList from '@/components/rewrite/MemberList.astro'; 5 + import SeasonLayout from '@/components/rewrite/SeasonLayout.astro'; 6 + import SkipLink from '@/components/rewrite/SkipLink.astro'; 7 + import BaseLayout from '@/layouts/rewrite/BaseLayout.astro'; 8 + import { getSeasonOGImage } from '@/utils/rewrite/images'; 9 + 10 + const { isRewrite } = Astro.locals; 11 + if (!isRewrite) { 12 + return Astro.redirect('/404'); 13 + } 14 + 15 + const seasonsCollection = await getCollection('seasonsRewrite'); 16 + const membersCollection = await getCollection('membersRewrite'); 17 + 18 + const pages = seasonsCollection.map((season) => { 19 + return { 20 + params: { season_id: season.id }, 21 + props: { 22 + season, 23 + members: membersCollection, 24 + }, 25 + }; 26 + }); 27 + 28 + const { season_id } = Astro.params; 29 + const page = pages.find((page) => page.params.season_id === season_id); 30 + if (!page) return Astro.redirect('/404'); 31 + const { season, members } = page.props; 32 + 33 + const pageTitle = `${season.data.title}`; 34 + 35 + const ogTitle = season.data.title; 36 + const ogDescription = `Watch all point of views of ${season.data.title}`; 37 + const ogImage = (await getSeasonOGImage(season.id)).default.src; 38 + const ogImageAlt = `The Life Series logo - ${season.data.title}`; 39 + --- 40 + 41 + <BaseLayout title={pageTitle}> 42 + <SEO 43 + slot="seo" 44 + title={ogTitle} 45 + description={ogDescription} 46 + openGraph={{ 47 + basic: { 48 + title: ogTitle, 49 + type: 'website', 50 + image: ogImage, 51 + }, 52 + image: { 53 + alt: ogImageAlt, 54 + }, 55 + optional: { 56 + description: ogDescription, 57 + siteName: 'The Life Series', 58 + }, 59 + }} 60 + twitter={{ 61 + card: 'summary', 62 + creator: '@ghustvn', 63 + title: ogTitle, 64 + description: ogDescription, 65 + image: ogImage, 66 + imageAlt: ogImageAlt, 67 + }} 68 + /> 69 + <SkipLink /> 70 + <SeasonLayout season={season}> 71 + <Fragment slot="sidebar-title">Members</Fragment> 72 + <MemberList slot="sidebar-nav" members={members} season={season} /> 73 + <Fragment slot="main-title">{season.data.title}</Fragment> 74 + </SeasonLayout> 75 + </BaseLayout>
+24 -93
src/pages/rewrite/seasons/[season_id]/[member_name].astro
··· 2 2 import { getCollection, getEntry } from 'astro:content'; 3 3 import { SEO } from 'astro-seo'; 4 4 import MemberItem from '@/components/rewrite/MemberItem.astro'; 5 + import MemberList from '@/components/rewrite/MemberList.astro'; 5 6 import SeasonLayout from '@/components/rewrite/SeasonLayout.astro'; 7 + import SkipLink from '@/components/rewrite/SkipLink.astro'; 6 8 import VideoItem from '@/components/rewrite/VideoItem.astro'; 7 9 import BaseLayout from '@/layouts/rewrite/BaseLayout.astro'; 8 10 import { getSeasonOGImage } from '@/utils/rewrite/images'; 9 - import { isCurrentPage } from '@/utils/url'; 10 11 11 12 const { isRewrite } = Astro.locals; 12 13 if (!isRewrite) { ··· 91 92 imageAlt: ogImageAlt, 92 93 }} 93 94 /> 95 + <SkipLink /> 94 96 <SeasonLayout season={season}> 95 97 { 96 98 season.data.sessionCount && ( ··· 100 102 ) 101 103 } 102 104 <Fragment slot="sidebar-title">Members</Fragment> 103 - <ul 104 - slot="sidebar-nav" 105 - role="list" 106 - aria-labelledby="sidebar-title" 107 - class="member-list" 108 - > 109 - { 110 - members.map((member) => { 111 - const isPlaying = Object.keys(season.data.videos).includes(member.data.name); 112 - 113 - return ( 114 - <li> 115 - <a 116 - class="member-item-link" 117 - role={!isPlaying ? 'link' : undefined} 118 - aria-disabled={!isPlaying ? 'true' : undefined} 119 - href={isPlaying ? `/rewrite/seasons/${season.id}/${member.data.name}` : undefined} 120 - aria-current={isPlaying ? isCurrentPage(Astro.url.pathname, `/rewrite/seasons/${season.id}/${member.data.name}`) : undefined} 121 - > 122 - <MemberItem memberName={member.data.name} /> 123 - </a> 124 - </li> 125 - ); 126 - }) 127 - } 128 - </ul> 105 + <MemberList slot="sidebar-nav" members={members} season={season} /> 129 106 <MemberItem slot="main-title" memberName={member.data.name} title={`${member.data.name} - ${season.data.title}`} /> 130 107 <div class="flow movie-session-wrapper"> 131 108 { ··· 156 133 </SeasonLayout> 157 134 </BaseLayout> 158 135 159 - <script> 136 + <script is:inline> 160 137 const currentPageLink = document.querySelector('[aria-current="page"]'); 161 - currentPageLink?.scrollIntoView({ block: 'center' }); 162 - </script> 163 138 164 - <style> 165 - .member-list { 166 - min-width: 0; 139 + if (currentPageLink) { 140 + let scrollable = currentPageLink.parentElement; 167 141 168 - &:has(.member-item-link:focus-visible) .member-item-link:not(:focus-visible) { 169 - filter: grayscale(75%); 170 - opacity: 0.8; 142 + while (scrollable) { 143 + const overflow = getComputedStyle(scrollable).overflowY; 144 + if (overflow === 'auto' || overflow === 'scroll') break; 145 + scrollable = scrollable.parentElement; 171 146 } 172 147 173 - @media (width < 48rem) { 174 - padding-inline-end: 12px; 175 - } 148 + if (scrollable && scrollable !== document.documentElement) { 149 + const prevBehavior = scrollable.style.scrollBehavior; 150 + scrollable.style.scrollBehavior = 'auto'; 176 151 177 - @media (hover) { 178 - &:has(.member-item-link:not([aria-disabled='true']):hover) 179 - .member-item-link:not(:hover) { 180 - filter: grayscale(75%); 181 - opacity: 0.8; 182 - } 152 + const linkTop = currentPageLink.getBoundingClientRect().top; 153 + const containerTop = scrollable.getBoundingClientRect().top; 154 + const containerHeight = scrollable.clientHeight; 155 + const offset = linkTop - containerTop - containerHeight / 2 + currentPageLink.clientHeight / 2; 156 + 157 + scrollable.scrollTop += offset; 158 + scrollable.style.scrollBehavior = prevBehavior; 183 159 } 184 160 } 185 161 186 - .member-item-link { 187 - display: grid; /* Fixes parent to be 1px taller */ 188 - justify-content: start; 189 - inline-size: 100%; 190 - padding-block: 4px; 191 - text-decoration: none; 192 - transition-property: filter, opacity; 193 - transition-duration: 100ms; 194 - transition-timing-function: linear; 195 - 196 - &:focus-visible { 197 - outline: none; 198 - 199 - &:not([aria-current='page']) { 200 - translate: 8px; 201 - } 202 - 203 - > div { 204 - outline: 2px solid var(--color-canvas-text); 205 - outline-offset: 4px; 206 - } 207 - } 162 + document.documentElement.classList.remove('js'); 163 + </script> 208 164 209 - @media (prefers-reduced-motion: no-preference) { 210 - transition: 211 - filter 100ms linear, 212 - opacity 100ms linear, 213 - translate var(--spring-transition-duration) var(--spring-transition-timing-function); 214 - } 215 - 216 - @media (hover) { 217 - &:hover:not([aria-current='page']):not([aria-disabled]) { 218 - translate: 8px; 219 - } 220 - } 221 - 222 - &[aria-current='page'] { 223 - translate: 12px; 224 - text-decoration-line: underline; 225 - text-decoration-style: wavy; 226 - font-weight: var(--font-weight-heading); 227 - } 228 - 229 - &[aria-disabled='true'] { 230 - filter: grayscale(75%); 231 - text-decoration: line-through 2px; 232 - } 233 - } 234 - 165 + <style> 235 166 .movie-session-wrapper { 236 167 margin-block-start: 16px; 237 168 }
+32 -2
src/pages/rewrite/seasons/[season_id]/sessions/[session_id].astro
··· 3 3 import { SEO } from 'astro-seo'; 4 4 import MemberItem from '@/components/rewrite/MemberItem.astro'; 5 5 import SeasonLayout from '@/components/rewrite/SeasonLayout.astro'; 6 + import SkipLink from '@/components/rewrite/SkipLink.astro'; 6 7 import VideoItem from '@/components/rewrite/VideoItem.astro'; 7 8 import BaseLayout from '@/layouts/rewrite/BaseLayout.astro'; 8 9 import { getSeasonOGImage } from '@/utils/rewrite/images'; ··· 88 89 imageAlt: ogImageAlt, 89 90 }} 90 91 /> 92 + <SkipLink /> 91 93 <SeasonLayout season={season}> 92 94 <Fragment slot="sidebar-title">Sessions</Fragment> 93 95 <ol ··· 131 133 </SeasonLayout> 132 134 </BaseLayout> 133 135 134 - <script> 136 + <script is:inline> 135 137 const currentPageLink = document.querySelector('[aria-current="page"]'); 136 - currentPageLink?.scrollIntoView({ block: 'center' }); 138 + 139 + if (currentPageLink) { 140 + let scrollable = currentPageLink.parentElement; 141 + 142 + while (scrollable) { 143 + const overflow = getComputedStyle(scrollable).overflowY; 144 + if (overflow === 'auto' || overflow === 'scroll') break; 145 + scrollable = scrollable.parentElement; 146 + } 147 + 148 + if (scrollable && scrollable !== document.documentElement) { 149 + const prevBehavior = scrollable.style.scrollBehavior; 150 + scrollable.style.scrollBehavior = 'auto'; 151 + 152 + const linkTop = currentPageLink.getBoundingClientRect().top; 153 + const containerTop = scrollable.getBoundingClientRect().top; 154 + const containerHeight = scrollable.clientHeight; 155 + const offset = linkTop - containerTop - containerHeight / 2 + currentPageLink.clientHeight / 2; 156 + 157 + scrollable.scrollTop += offset; 158 + scrollable.style.scrollBehavior = prevBehavior; 159 + } 160 + } 161 + 162 + document.documentElement.classList.remove('js'); 137 163 </script> 138 164 139 165 <style> ··· 141 167 &:has(.session-item-link:focus-visible) .session-item-link:not(:focus-visible) { 142 168 filter: grayscale(75%); 143 169 opacity: 0.8; 170 + } 171 + 172 + @media (width < 48rem) { 173 + padding-inline-end: 12px; 144 174 } 145 175 146 176 @media (hover) {
+7
src/utils/rewrite/images.ts
··· 12 12 )(); 13 13 } 14 14 15 + export async function getSeasonImage(season: string) { 16 + const seasonImages = import.meta.glob<{ default: ImageMetadata }>( 17 + '/src/assets/rewrite/images/seasons/*', 18 + ); 19 + return seasonImages[`/src/assets/rewrite/images/seasons/${season}.webp`](); 20 + } 21 + 15 22 export async function getSeasonOGImage(season: string) { 16 23 const seasonImages = import.meta.glob<{ default: ImageMetadata }>( 17 24 '/src/assets/rewrite/images/og/seasons/*',