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.

Updates, session pages for Past Life (#6)

authored by

Ghustvn and committed by
GitHub
9f4ecfc7 196f42fb

+374 -89
+1
src/components/SeasonList.astro
··· 14 14 <ol 15 15 role="list" 16 16 class="seasons-list grid gap-24 overflow-x-clip px-24 py-16" 17 + reversed 17 18 > 18 19 { 19 20 seasons.map((season) => (
+122
src/components/SessionList.astro
··· 1 + --- 2 + import type { CollectionEntry } from 'astro:content'; 3 + import { isCurrentPage } from '@/utils/url'; 4 + 5 + interface Props { 6 + season: CollectionEntry<'seasons'>; 7 + sessionNumbers: number[]; 8 + withText: boolean; 9 + } 10 + 11 + const { season, sessionNumbers, withText } = Astro.props; 12 + --- 13 + 14 + <ol 15 + role="list" 16 + reversed 17 + class:list={['session-list', { 'flex gap-8': !withText, 'grid gap-16': withText }]} 18 + > 19 + { 20 + sessionNumbers.map((session) => ( 21 + <li> 22 + <a 23 + href={`/seasons/${season.data.seasonNumber}/sessions/${session}`} 24 + class:list={[ 25 + 'bg-canvas-text/5 flex items-center justify-center rounded-full p-8 shadow-xs', 26 + { 'mx-6 aspect-square h-44': !withText }, 27 + { 'px-16 text-lg': withText }, 28 + ]} 29 + aria-current={isCurrentPage( 30 + Astro.url.pathname, 31 + `/seasons/${season.id}/sessions/${session}`, 32 + )} 33 + > 34 + <span class="flex gap-6"> 35 + <span class:list={[{ 'sr-only': !withText }]}>Session</span> 36 + <span>{session}</span> 37 + </span> 38 + </a> 39 + </li> 40 + )) 41 + } 42 + </ol> 43 + 44 + <script> 45 + const currentPageElement = document.querySelector('.session-list a[aria-current="page"]'); 46 + currentPageElement?.scrollIntoView({ block: 'end', inline: 'center' }); 47 + </script> 48 + 49 + <style> 50 + .session-list { 51 + a { 52 + transition-property: filter, opacity; 53 + transition-duration: 100ms; 54 + transition-timing-function: linear; 55 + outline-offset: -6px; 56 + } 57 + 58 + a[aria-current='page'] { 59 + outline: 2px solid; 60 + scale: 1.075; 61 + rotate: var(--angle); 62 + } 63 + 64 + &:has(a:focus-visible) a:not(:focus-visible) { 65 + filter: grayscale(75%); 66 + opacity: 0.8; 67 + } 68 + 69 + @media (hover) { 70 + &:has(a:hover) a:not(:hover) { 71 + filter: grayscale(75%); 72 + opacity: 0.8; 73 + } 74 + } 75 + 76 + @media (prefers-reduced-motion: no-preference) { 77 + li:nth-child(1) { 78 + --angle: 3deg; 79 + } 80 + li:nth-child(2) { 81 + --angle: -2deg; 82 + } 83 + li:nth-child(3) { 84 + --angle: 2deg; 85 + } 86 + li:nth-child(4) { 87 + --angle: -1deg; 88 + } 89 + li:nth-child(5) { 90 + --angle: 2deg; 91 + } 92 + li:nth-child(6) { 93 + --angle: -3deg; 94 + } 95 + li:nth-child(7) { 96 + --angle: 1deg; 97 + } 98 + 99 + a { 100 + transition-property: filter, opacity, scale, rotate; 101 + transition-duration: 102 + 100ms, 100ms, var(--spring-transition-duration), 103 + var(--spring-transition-duration); 104 + transition-timing-function: 105 + linear, linear, var(--spring-transition-timing-function), 106 + var(--spring-transition-timing-function); 107 + } 108 + 109 + a:focus-visible:not([aria-current='page']) { 110 + scale: 1.075; 111 + rotate: var(--angle); 112 + } 113 + } 114 + 115 + @media (hover) and (prefers-reduced-motion: no-preference) { 116 + a:hover:not([aria-current='page']) { 117 + scale: 1.075; 118 + rotate: var(--angle); 119 + } 120 + } 121 + } 122 + </style>
+1
src/content.config.ts
··· 6 6 schema: z.object({ 7 7 title: z.string(), 8 8 seasonNumber: z.number(), 9 + sessionCount: z.number().optional(), 9 10 seo: z.object({ 10 11 description: z.object({ 11 12 twist: z.string(),
+1
src/data/seasons.json
··· 3 3 "id": "7", 4 4 "title": "Past Life", 5 5 "seasonNumber": 7, 6 + "sessionCount": 3, 6 7 "seo": { 7 8 "description": { 8 9 "twist": "The first episode starts in Minecraft version beta 1.2_02 and every new episode the Minecraft version gets updated to a newer one until eventually the most recent version."
+3
src/layouts/BaseLayout.astro
··· 9 9 } 10 10 11 11 const { title } = Astro.props; 12 + 13 + Astro.response.headers.set('Cache-Control', 'public, max-age=0, s-maxage=86400'); 14 + Astro.response.headers.set('CDN-Cache-Control', 'public, max-age=0, s-maxage=86400'); 12 15 --- 13 16 14 17 <html lang="en">
+5 -2
src/layouts/SeasonsLayout.astro
··· 18 18 19 19 const seasons = await getCollection('seasons'); 20 20 const members = await getCollection('members'); 21 + 22 + Astro.response.headers.set('Cache-Control', 'public, max-age=0, s-maxage=86400'); 23 + Astro.response.headers.set('CDN-Cache-Control', 'public, max-age=0, s-maxage=86400'); 21 24 --- 22 25 23 26 <html ··· 100 103 </div> 101 104 <div class="grid min-h-svh items-start md:grid-cols-[auto_auto_1fr]"> 102 105 <div 103 - class="custom-scrollbar-gutter-stable sticky top-0 hidden h-full max-h-svh overflow-y-auto md:grid" 106 + class="custom-scrollbar-gutter-stable sticky top-0 hidden h-full max-h-svh overflow-y-auto overscroll-y-none md:grid" 104 107 > 105 108 <div class="self-center"> 106 109 <h2 ··· 115 118 </div> 116 119 </div> 117 120 <div 118 - class="custom-scrollbar-gutter-stable sticky top-0 hidden h-full max-h-svh overflow-y-auto pl-2 md:grid" 121 + class="custom-scrollbar-gutter-stable sticky top-0 hidden h-full max-h-svh overflow-y-auto overscroll-y-none pl-2 md:grid" 119 122 > 120 123 <div class="self-center"> 121 124 <h2
+1
src/pages/index.astro
··· 19 19 <ol 20 20 role="list" 21 21 class="seasons-list grid grid-cols-[repeat(auto-fit,minmax(min(180px,100%),1fr))] place-items-center gap-48" 22 + reversed 22 23 > 23 24 { 24 25 seasons.map((season) => (
+131
src/pages/seasons/7/sessions/[session_id].astro
··· 1 + --- 2 + import { Image } from 'astro:assets'; 3 + import { getEntry, type CollectionEntry } from 'astro:content'; 4 + import { SEO } from 'astro-seo'; 5 + import { YouTube } from '@astro-community/astro-embed-youtube'; 6 + import SeasonsLayout from '@/layouts/SeasonsLayout.astro'; 7 + import SessionList from '@/components/SessionList.astro'; 8 + import { getMemberImage } from '@/utils/images'; 9 + 10 + export async function getStaticPaths() { 11 + function createSeasonSessionList(season: CollectionEntry<'seasons'>, sessionNumber: number) { 12 + return Object.entries(season.data.videos).map(([member, memberVideos]) => { 13 + return { 14 + [member]: memberVideos.filter((video) => !video.isMovie)[sessionNumber - 1], 15 + }; 16 + }); 17 + } 18 + 19 + const season = await getEntry('seasons', '7'); 20 + if (!season) { 21 + return; 22 + } 23 + 24 + const sessionNumbers = season.data.sessionCount 25 + ? Array.from(Array(season.data.sessionCount).keys()) 26 + .map((sessionIndex) => ++sessionIndex) 27 + .reverse() 28 + : []; 29 + 30 + return sessionNumbers.map((sessionNumber) => { 31 + return { 32 + params: { season_id: season.id, session_id: sessionNumber }, 33 + props: { 34 + season: season, 35 + sessions: createSeasonSessionList(season, sessionNumber), 36 + sessionNumbers, 37 + sessionNumber, 38 + }, 39 + }; 40 + }); 41 + } 42 + 43 + const { season, sessions, sessionNumbers, sessionNumber } = Astro.props; 44 + 45 + const title = `Session ${sessionNumber} · ${season.data.title} (S${season.data.seasonNumber})`; 46 + --- 47 + 48 + <SeasonsLayout 49 + title={title} 50 + season={season} 51 + > 52 + <SEO 53 + slot="seo" 54 + description={`Session ${sessionNumber} of ${season.data.title} (S${season.data.seasonNumber}).`} 55 + /> 56 + <div class="@container pt-8"> 57 + <h2 58 + id="all-sessions-title" 59 + class="mx-fluid-v-320-768-16-24 md:mx-fluid-v-768-1930-24-128 text-base font-normal" 60 + > 61 + {season.data.title} (S{season.data.seasonNumber}) sessions 62 + </h2> 63 + <div 64 + class="mx-fluid-v-320-768-16-24 md:mx-fluid-v-768-1930-24-128 bg-canvas border-canvas-text/25 sticky top-0 z-1 max-w-720 overflow-x-auto overflow-y-clip border-b-1 py-12 motion-safe:scroll-smooth" 65 + > 66 + <nav aria-labelledby="all-sessions-title"> 67 + <SessionList 68 + season={season} 69 + sessionNumbers={sessionNumbers} 70 + withText={false} 71 + /> 72 + </nav> 73 + </div> 74 + <main> 75 + <div 76 + class="mx-fluid-v-320-768-16-24 md:mx-fluid-v-768-1930-24-128 pt-fluid-v-320-768-16-24 md:pt-fluid-c-320-1930-24-80 max-w-720 pb-[50vh]" 77 + > 78 + <h1 79 + id="main-content" 80 + tabindex="-1" 81 + class="custom-trim-start mb-8 w-fit text-base text-balance" 82 + > 83 + Session {sessionNumber} of {season.data.title} (S{season.data.seasonNumber}) 84 + </h1> 85 + <ul 86 + role="list" 87 + aria-labelledby="main-content" 88 + class="space-y-fluid-c-320-768-24-64" 89 + > 90 + { 91 + sessions.map((session) => { 92 + return Object.entries(session).map( 93 + ([member, video]) => 94 + video && ( 95 + <li> 96 + <h2 class="grid gap-y-4 pb-8 text-base font-normal text-balance"> 97 + <div class="flex items-center gap-8"> 98 + <Image 99 + src={getMemberImage(member)} 100 + width="64" 101 + height="64" 102 + alt="" 103 + loading="eager" 104 + class="aspect-square max-w-32 rounded-full" 105 + /> 106 + <span>{member}</span> 107 + </div> 108 + <span class="text-h3">{video.title}</span> 109 + </h2> 110 + {import.meta.env.PROD ? ( 111 + <YouTube 112 + id={video.id} 113 + title={video.title} 114 + poster={`https://i.ytimg.com/vi_webp/${video.id}/maxresdefault.webp`} 115 + playlabel={`Play video: ${video.title}`} 116 + posterQuality="max" 117 + class="rounded-lg" 118 + /> 119 + ) : ( 120 + <div class="bg-canvas-text aspect-video max-w-720 rounded-lg" /> 121 + )} 122 + </li> 123 + ), 124 + ); 125 + }) 126 + } 127 + </ul> 128 + </div> 129 + </main> 130 + </div> 131 + </SeasonsLayout>
+26 -7
src/pages/seasons/[season_id].astro
··· 2 2 import { getCollection } from 'astro:content'; 3 3 import { SEO } from 'astro-seo'; 4 4 import SeasonsLayout from '@/layouts/SeasonsLayout.astro'; 5 + import SessionList from '@/components/SessionList.astro'; 5 6 6 7 export async function getStaticPaths() { 7 8 const seasons = await getCollection('seasons'); 8 9 9 - return seasons.map((season) => ({ 10 - params: { season_id: season.id }, 11 - props: { season }, 12 - })); 10 + return seasons.map((season) => { 11 + const sessions = season.data.sessionCount 12 + ? Array.from(Array(season.data.sessionCount).keys()) 13 + .map((sessionIndex) => ++sessionIndex) 14 + .reverse() 15 + : []; 16 + 17 + return { 18 + params: { season_id: season.id }, 19 + props: { season, sessions }, 20 + }; 21 + }); 13 22 } 14 23 15 - const { season } = Astro.props; 24 + const { season, sessions } = Astro.props; 16 25 17 26 const title = `${season.data.title} (S${season.data.seasonNumber})`; 18 27 --- ··· 27 36 /> 28 37 <main class="@container h-full"> 29 38 <div 30 - class="mx-fluid-v-320-768-16-24 md:mx-fluid-v-768-1930-24-128 grid h-full place-content-center justify-items-center gap-8 text-center text-balance" 39 + class="mx-fluid-v-320-768-16-24 md:mx-fluid-v-768-1930-24-128 grid h-full place-content-center justify-items-center gap-16 py-16 text-center text-balance" 31 40 > 32 41 <h1 33 42 id="main-content" ··· 36 45 > 37 46 {season.data.title} (S{season.data.seasonNumber}) 38 47 </h1> 39 - <p>Select a member from navigation</p> 48 + { 49 + sessions.length > 0 ? ( 50 + <SessionList 51 + season={season} 52 + sessionNumbers={sessions} 53 + withText 54 + /> 55 + ) : ( 56 + <p>Select a member from navigation</p> 57 + ) 58 + } 40 59 </div> 41 60 </main> 42 61 </SeasonsLayout>
+70 -69
src/pages/seasons/[season_id]/[member_name].astro
··· 45 45 const sessions = videos.filter((video) => !video.isMovie); 46 46 47 47 const title = `${member.data.name} · ${season.data.title} (S${season.data.seasonNumber})`; 48 - 49 - Astro.response.headers.set('Cache-Control', 'max-age=0, s-maxage=86400'); 50 48 --- 51 49 52 50 <SeasonsLayout ··· 56 54 > 57 55 <SEO 58 56 slot="seo" 59 - description={`${member.data.name}'s point of view for ${season.data.title} (S${season.data.seasonNumber}).`} 57 + description={`${member.data.name} point of view for ${season.data.title} (S${season.data.seasonNumber}).`} 60 58 /> 61 59 <main class="@container"> 62 - <h1 63 - id="main-content" 64 - class="sr-only" 65 - > 66 - {title} 67 - </h1> 68 - <div 69 - class="mx-fluid-v-320-768-16-24 md:mx-fluid-v-768-1930-24-128 py-fluid-v-320-768-16-24 md:py-fluid-c-320-1930-24-80 max-w-1080" 70 - > 71 - { 72 - movie && ( 73 - <> 74 - <h2 class="custom-trim-start mb-8 text-base font-bold">The Movie</h2> 75 - {import.meta.env.PROD ? ( 76 - <YouTube 77 - id={movie.id} 78 - title={movie.title} 79 - poster={`https://i.ytimg.com/vi_webp/${movie.id}/maxresdefault.webp`} 80 - playlabel={`Play video: ${movie.title}`} 81 - posterQuality="max" 82 - class="movie-video rounded-lg" 83 - /> 84 - ) : ( 85 - <div class="bg-canvas-text aspect-video max-w-1080 rounded-lg" /> 86 - )} 87 - <hr class="my-fluid-c-320-1930-24-80 border-canvas-text/25 border-1" /> 88 - </> 89 - ) 90 - } 91 - { 92 - sessions.length > 0 && ( 93 - <> 94 - <h2 95 - id="session-title" 96 - class="custom-trim-start mb-8 text-base font-bold" 97 - > 98 - Episodes 99 - </h2> 100 - <ol 101 - role="list" 102 - aria-labelledby="session-title" 103 - class="space-y-fluid-c-320-768-24-64" 104 - > 105 - {sessions.map((session) => ( 106 - <li> 107 - <h3 class="text-h3 pb-8 font-normal text-balance"> 108 - {session.title} 109 - </h3> 110 - {import.meta.env.PROD ? ( 111 - <YouTube 112 - id={session.id} 113 - title={session.title} 114 - poster={`https://i.ytimg.com/vi_webp/${session.id}/maxresdefault.webp`} 115 - playlabel={`Play video: ${session.title}`} 116 - posterQuality="max" 117 - class="rounded-lg" 118 - /> 119 - ) : ( 120 - <div class="bg-canvas-text aspect-video max-w-720 rounded-lg" /> 121 - )} 122 - </li> 123 - ))} 124 - </ol> 125 - </> 126 - ) 127 - } 60 + <div class="mx-fluid-v-320-768-16-24 md:mx-fluid-v-768-1930-24-128 max-w-1080 pb-[50vh]"> 61 + <div class="bg-canvas border-canvas-text/25 sticky top-0 z-1 border-b-1 py-8"> 62 + <h1 63 + id="main-content" 64 + class="w-fit text-base font-normal text-balance" 65 + tabindex="-1" 66 + > 67 + {title} 68 + </h1> 69 + </div> 70 + <div class="pt-fluid-v-320-768-16-24 md:pt-fluid-c-320-1930-24-80"> 71 + { 72 + movie && ( 73 + <> 74 + <h2 class="custom-trim-start mb-8 text-base">The Movie</h2> 75 + {import.meta.env.PROD ? ( 76 + <YouTube 77 + id={movie.id} 78 + title={movie.title} 79 + poster={`https://i.ytimg.com/vi_webp/${movie.id}/maxresdefault.webp`} 80 + playlabel={`Play video: ${movie.title}`} 81 + posterQuality="max" 82 + class="movie-video rounded-lg" 83 + /> 84 + ) : ( 85 + <div class="bg-canvas-text aspect-video max-w-1080 rounded-lg" /> 86 + )} 87 + <hr class="my-fluid-c-320-1930-24-80 border-canvas-text/25 border-x-0 border-t-0 border-solid" /> 88 + </> 89 + ) 90 + } 91 + { 92 + sessions.length > 0 && ( 93 + <> 94 + <h2 95 + id="session-title" 96 + class="custom-trim-start mb-8 text-base" 97 + > 98 + Episodes 99 + </h2> 100 + <ol 101 + role="list" 102 + aria-labelledby="session-title" 103 + class="space-y-fluid-c-320-768-24-64" 104 + > 105 + {sessions.map((session) => ( 106 + <li> 107 + <h3 class="text-h3 pb-8 font-normal text-balance"> 108 + {session.title} 109 + </h3> 110 + {import.meta.env.PROD ? ( 111 + <YouTube 112 + id={session.id} 113 + title={session.title} 114 + poster={`https://i.ytimg.com/vi_webp/${session.id}/maxresdefault.webp`} 115 + playlabel={`Play video: ${session.title}`} 116 + posterQuality="max" 117 + class="rounded-lg" 118 + /> 119 + ) : ( 120 + <div class="bg-canvas-text aspect-video max-w-720 rounded-lg" /> 121 + )} 122 + </li> 123 + ))} 124 + </ol> 125 + </> 126 + ) 127 + } 128 + </div> 128 129 </div> 129 130 </main> 130 131 </SeasonsLayout>
+13 -11
src/styles/global.css
··· 134 134 135 135 .seasons-list { 136 136 img { 137 - transition-property: filter, blur, opacity; 137 + transition-property: filter, opacity; 138 138 transition-duration: 100ms; 139 139 transition-timing-function: linear; 140 140 } ··· 150 150 } 151 151 152 152 &:has(a:focus-visible) a:not(:focus-visible) img { 153 - filter: grayscale(75%) blur(0.3px); 153 + filter: grayscale(75%); 154 154 opacity: 0.8; 155 155 } 156 156 157 157 @media (hover) { 158 158 &:has(a:hover) a:not(:hover) img { 159 - filter: grayscale(75%) blur(0.3px); 159 + filter: grayscale(75%); 160 160 opacity: 0.8; 161 161 } 162 162 } ··· 207 207 208 208 .members-list { 209 209 a { 210 - transition-property: filter, blur, opacity; 210 + transition-property: filter, opacity; 211 211 transition-duration: 100ms; 212 212 transition-timing-function: linear; 213 213 214 - &[aria-current='page']:not([data-not-playing]) span { 215 - text-decoration: underline wavy 2px; 214 + &[aria-current='page'] { 215 + text-decoration-line: underline; 216 + text-decoration-thickness: 2px; 217 + text-decoration-style: wavy; 216 218 text-underline-offset: 4px; 217 219 } 218 220 } 219 221 220 - &:has(a:not([data-not-playing]):focus-visible) a:not(:focus-visible) { 221 - filter: grayscale(75%) blur(0.3px); 222 + &:has(a:focus-visible) a:not(:focus-visible) { 223 + filter: grayscale(75%); 222 224 opacity: 0.8; 223 225 } 224 226 225 227 @media (hover) { 226 228 &:has(a:not([data-not-playing]):hover) a:not(:hover) { 227 - filter: grayscale(75%) blur(0.3px); 229 + filter: grayscale(75%); 228 230 opacity: 0.8; 229 231 } 230 232 } ··· 235 237 transition-timing-function: var(--spring-transition-timing-function); 236 238 } 237 239 238 - a:focus-visible:not([aria-current='page']):not([data-not-playing]) div { 240 + a:focus-visible:not([aria-current='page']) div { 239 241 translate: 8px; 240 242 } 241 243 } ··· 246 248 } 247 249 } 248 250 249 - [aria-current='page']:not([data-not-playing]) div { 251 + [aria-current='page'] div { 250 252 translate: 12px; 251 253 } 252 254 }