Add timeline

pull/1/head
Made Baruna 2021-03-12 19:16:03 +08:00
parent fe3ea7b9fd
commit 21a39b5d34
14 changed files with 460 additions and 3 deletions

View File

@ -77,6 +77,13 @@
label="Todo List"
href="/todo"
/>
<SidebarItem
on:clicked={close}
active={segment === 'timeline'}
image="/images/timeline.png"
label="Timeline"
href="/timeline"
/>
<SidebarItem
on:clicked={close}
active={segment === 'settings'}

64
src/data/timeline.js Normal file
View File

@ -0,0 +1,64 @@
export const eventsData = [
[
{
name: 'Vishaps and Where to Find Them',
image: 'vishaps_and_where_to_find_them.jpg',
pos: '0% 30%',
start: '2021-03-05 04:00:00',
end: '2021-03-12 04:00:00',
color: '#F6AD55',
},
{
name: 'Update 1.4!',
image: 'update14.png',
pos: '0% 23%',
start: '2021-03-17 11:00:00',
end: '2021-03-24 04:00:00',
startOnly: true,
color: '#79D2EB',
zoom: '120%',
},
],
{
name: 'Moment of Bloom - Hu Tao Banner',
pos: '50% 20%',
image: 'moment_of_bloom.jpg',
start: '2021-03-02 18:00:00',
end: '2021-03-16 15:00:00',
color: '#FC8181',
},
{
name: 'Epitome Invocation - Weapon Banner',
image: 'epitome_invocation.jpg',
pos: '50% 20%',
start: '2021-02-23 18:00:00',
end: '2021-03-16 15:00:00',
color: '#F56565',
},
[
{
name: 'Spiral Abyss',
image: 'spiral_abyss.jpg',
pos: '50% 20%',
start: '2021-03-01 04:00:00',
end: '2021-03-16 04:00:00',
color: '#63B3ED',
},
{
name: 'Spiral Abyss',
image: 'spiral_abyss.jpg',
pos: '50% 20%',
start: '2021-03-16 04:00:00',
end: '2021-04-01 04:00:00',
color: '#4299E1',
},
],
{
name: 'Battle Pass',
image: 'lantern-lit_sky.jpg',
pos: '0% 12%',
start: '2021-02-03 11:00:00',
end: '2021-03-15 04:00:00',
color: '#68D391',
},
];

View File

@ -0,0 +1,48 @@
<script>
import dayjs from 'dayjs';
import { onMount } from 'svelte';
export let event;
let now = dayjs();
onMount(() => {
const interval = setInterval(() => {
now = dayjs();
}, 1000);
return () => {
clearInterval(interval);
};
});
$: started = now.isAfter(event.start);
$: ended = now.isAfter(event.end);
$: diffStart = event.start.diff(now);
$: diffEnd = event.end.diff(now);
</script>
<div>
<img src="/images/events/{event.image}" class="w-full rounded-lg" alt={event.name} />
<h1 class="mt-4 text-white font-display font-semibold text-xl">{event.name}</h1>
<p class="text-gray-400 font-body flex flex-col md:flex-row">
<span class="flex">
<span>{event.start.format('ddd, D MMM YYYY HH:mm')}</span>
{#if !event.startOnly}<span class="mx-2">-</span>{/if}
</span>
{#if !event.startOnly}
<span>{event.end.format('ddd, D MMM YYYY HH:mm')}</span>
{/if}
</p>
<p class="text-gray-400 px-4 py-1 bg-black bg-opacity-50 rounded-xl mt-2 inline-block">
{#if !started}
Starting in {dayjs.duration(diffStart).format(diffStart > 86400000 ? 'D[d] HH:mm:ss' : 'HH:mm:ss')}
{:else if started && !ended && !event.startOnly}
Ending in {dayjs.duration(diffEnd).format(diffEnd > 86400000 ? 'D[d] HH:mm:ss' : 'HH:mm:ss')}
{:else if event.startOnly}
Live Now!
{:else}
Finished
{/if}
</p>
</div>

View File

@ -0,0 +1,78 @@
<script>
import dayjs from 'dayjs';
export let prev = null;
export let next = null;
export let event;
export let openDetail;
export let dayWidth;
export let marginTop;
export let eventHeight;
export let eventMargin;
export let now;
export let i;
$: prevNearby = prev !== null && event.start.diff(prev.end, 'hour') < 12;
$: nextNearby = next !== null && next.start.diff(event.end, 'hour') < 12;
$: started = now.isAfter(event.start);
$: ended = now.isAfter(event.end);
$: diffStart = event.start.diff(now);
$: diffEnd = event.end.diff(now);
</script>
<div
on:click={openDetail}
class="flex items-center z-10 text-white cursor-pointer absolute {prevNearby ? '' : 'rounded-l-xl'} {nextNearby
? 'border-r-4 border-white'
: 'rounded-r-xl'}"
style="width: {dayWidth * event.duration}px; left: {dayWidth *
event.offset}px; background-color: {event.color};
top: {marginTop +
i * (eventHeight + eventMargin)}px; height: {eventHeight}px; padding-right: 10px;
{prevNearby &&
diffStart > 86400000
? 'padding-left: 50px;'
: 'padding-left: 10px;'}
--image: url(/images/events/{event.image}); --pos: {event.pos}; --color: {event.color};
--zoom: {event.zoom ? event.zoom : '200%'};"
>
<div class="event-item {nextNearby ? '' : 'rounded-xl'}" />
<span class="event-name text sticky left-0 font-display text-base md:text-lg text-black font-bold whitespace-no-wrap">
{event.name}
</span>
{#if started && !ended && !event.startOnly}
<div class="absolute pl-3" style="top: 6px; right: -200px; width: 200px;">
<span class="text-sm rounded-xl text-black font-semibold bg-white bg-opacity-75 px-1">
{dayjs.duration(diffEnd).format(diffEnd > 86400000 ? 'D[d] HH:mm:ss' : 'HH:mm:ss')}
</span>
</div>
{:else if !started && !ended}
<div class="absolute pr-3 text-right" style="top: 6px; left: {prevNearby ? '-150px' : '-200px'}; width: 200px;">
<span class="text-sm rounded-xl text-black font-semibold bg-white bg-opacity-75 px-1">
{dayjs.duration(diffStart).format(diffStart > 86400000 ? 'D[d] HH:mm:ss' : 'HH:mm:ss')}
</span>
</div>
{/if}
</div>
<style>
div.event-item {
position: absolute;
opacity: 1;
right: 0;
top: 0;
width: 100%;
max-width: 200px;
height: 100%;
background-image: var(--image);
background-position: var(--pos);
background-repeat: no-repeat;
background-size: var(--zoom);
mask-image: linear-gradient(to left, rgba(0, 0, 0, 1), rgba(0, 0, 0, 0));
}
span.event-name {
text-shadow: var(--color) -1px -1px 4px, var(--color) 1px -1px 4px, var(--color) -1px 1px 4px,
var(--color) 1px 1px 4px, var(--color) 0 0 10px;
}
</style>

View File

@ -0,0 +1,259 @@
<script>
import { getContext, onMount, tick } from 'svelte';
import dayjs from 'dayjs';
import duration from 'dayjs/plugin/duration';
dayjs.extend(duration);
import { eventsData } from '../../data/timeline';
import EventItem from './_item.svelte';
import DetailModal from './_detail.svelte';
const { open: openModal } = getContext('simple-modal');
function openDetail(event) {
openModal(
DetailModal,
{
event,
},
{
closeButton: false,
styleWindow: { background: '#25294A', width: '600px' },
},
);
}
let timelineContainer;
let dayWidth = 50;
const eventHeight = 36;
const eventMargin = 20;
const padding = 10;
const marginTop = 80;
let lastEventTime = dayjs().year(2000);
let firstDay;
let dates = [];
let months = {};
function convertToDate(e, i) {
const start = dayjs(e.start, 'YYYY-MM-DD HH:mm:ss');
const end = dayjs(e.end, 'YYYY-MM-DD HH:mm:ss');
const duration = end.diff(start, 'day', true);
if (lastEventTime < end) lastEventTime = end;
return {
...e,
index: i,
start,
end,
duration,
};
}
let events = eventsData.map((e, i) => {
if (Array.isArray(e)) {
return e.map((item) => convertToDate(item, i));
}
return convertToDate(e, i);
});
events
.slice()
.sort((a, b) => {
if (Array.isArray(a) && Array.isArray(b)) {
return a[0].start - b[0].start;
} else if (!Array.isArray(a) && Array.isArray(b)) {
return a.start - b[0].start;
} else if (Array.isArray(a) && !Array.isArray(b)) {
return a[0].start - b.start;
} else {
return a.start - b.start;
}
})
.forEach((e, i) => {
if (i === 0) {
if (Array.isArray(e)) {
firstDay = e[0].start.set('hour', 0).set('minute', 0).set('second', 0).subtract(padding, 'day');
} else {
firstDay = e.start.set('hour', 0).set('minute', 0).set('second', 0).subtract(padding, 'day');
}
}
if (Array.isArray(e)) {
for (let j = 0; j < e.length; j++) {
const current = e[j];
const offset = Math.abs(firstDay.diff(events[current.index][j].start, 'day', true));
events[current.index][j].offset = offset;
}
} else {
const offset = Math.abs(firstDay.diff(e.start, 'day', true));
events[e.index].offset = offset;
}
});
let today = dayjs();
$: todayOffset = Math.abs(firstDay.diff(today, 'day', true));
const dayTotal = Math.abs(Math.ceil(firstDay.diff(lastEventTime, 'day', true))) + 2 * padding;
for (let i = 0; i < dayTotal; i++) {
const month = firstDay.add(i, 'day').format('MMMM');
if (months[month] === undefined) {
months[month] = {
total: 0,
offset: 0,
};
}
months[month].total++;
}
const monthList = Object.entries(months);
for (let i = 0; i < monthList.length; i++) {
monthList[i][1].offset = i - 1 >= 0 ? monthList[i - 1][1].total + monthList[i - 1][1].offset : 0;
}
dates = [...new Array(dayTotal)].map((_, i) => firstDay.add(i, 'day').date());
onMount(() => {
console.log(firstDay);
console.log(events);
if (timelineContainer.offsetWidth < 500) {
dayWidth = 40;
tick();
}
timelineContainer.scrollTo({
left: todayOffset * dayWidth - timelineContainer.offsetWidth / 2 + dayWidth,
top: 0,
behavior: 'smooth',
});
const interval = setInterval(() => {
today = dayjs();
}, 1000);
return () => {
clearInterval(interval);
};
});
function transformScroll(event) {
if (!event.deltaY) {
return;
}
event.currentTarget.scrollLeft += event.deltaY;
}
</script>
<svelte:head>
<title>Timeline - Paimon.moe</title>
<meta
name="description"
content="Genshin Impact event timeline calendar, view when an event and abyys order will start and end with neat timeline"
/>
<meta
property="og:description"
content="Genshin Impact event timeline calendar, view when an event and abyys order will start and end with neat timeline"
/>
</svelte:head>
<div class="lg:ml-64 pt-20 lg:pt-8">
<h1 class="font-display px-4 md:px-8 font-black text-5xl text-white">Timeline</h1>
<div class="w-full overflow-x-auto px-4 md:px-8" bind:this={timelineContainer} on:mousewheel={transformScroll}>
<div
style={`padding-top: 50px; width: min-content; padding-right: ${2 * padding * dayWidth}px; height: ${
marginTop + events.length * (eventHeight + eventMargin)
}px`}
class="timeline flex flex-col relative content"
>
<!-- DATE BAR -->
{#each dates as date, i}
<div
class="bg-gray-700"
style={`width: 1px; height: calc(100% - ${eventHeight}px); position: absolute;
left: ${i * dayWidth}px; top: ${eventHeight}px;`}
>
<span
class="absolute top-0 text-gray-200 text-center pb-1 bg-background-secondary"
style="width: 20px; left: -10px;"
>
{date}
</span>
</div>
{/each}
<!-- MONTH TITLE -->
{#each monthList as [month, item]}
<div
class="absolute bg-background-secondary pr-4"
style={`top: 12px; width: ${item.total * dayWidth}px; left: ${item.offset * dayWidth}px;`}
>
<span class="text-legendary-from font-bold sticky left-0">{month}</span>
</div>
{/each}
<!-- EVENT STRIP -->
{#each events as event, i}
{#if Array.isArray(event)}
{#each event as item, j}
<EventItem
prev={j > 0 ? event[j - 1] : null}
next={j < event.length - 1 ? event[j + 1] : null}
now={today}
event={item}
openDetail={() => openDetail(item)}
{dayWidth}
{marginTop}
{eventHeight}
{eventMargin}
{i}
/>
{/each}
{:else}
<EventItem
now={today}
openDetail={() => openDetail(event)}
{event}
{dayWidth}
{marginTop}
{eventHeight}
{eventMargin}
{i}
/>
{/if}
{/each}
<!-- NOW BAR -->
<div
class="bg-gray-200 z-20 relative opacity-75"
style={`left: ${
todayOffset * dayWidth
}px; width: 2px; height: calc(100% - 10px); position: absolute; top: 10px;`}
>
<div class="absolute rounded-xl top-0 text-center bg-white text-black" style="width: 80px; left: -40px;">
{today.format('HH:mm:ss')}
</div>
</div>
</div>
</div>
</div>
<style>
::-webkit-scrollbar {
height: 8px;
}
::-webkit-scrollbar-track {
@apply bg-transparent;
margin: 0 20px;
}
::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.35);
@apply rounded-xl;
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

BIN
static/images/timeline.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

@ -4,3 +4,4 @@ https://paimon.moe/wish
https://paimon.moe/calculator
https://paimon.moe/items
https://paimon.moe/todo
https://paimon.moe/timeline

View File

@ -1278,9 +1278,9 @@ cssesc@^3.0.0:
integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
dayjs@^1.9.4:
version "1.9.4"
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.9.4.tgz#fcde984e227f4296f04e7b05720adad2e1071f1b"
integrity sha512-ABSF3alrldf7nM9sQ2U+Ln67NRwmzlLOqG7kK03kck0mw3wlSSEKv/XhKGGxUjQcS57QeiCyNdrFgtj9nWlrng==
version "1.10.4"
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.4.tgz#8e544a9b8683f61783f570980a8a80eaf54ab1e2"
integrity sha512-RI/Hh4kqRc1UKLOAf/T5zdMMX5DQIlDxwUe3wSyMMnEbGunnpENCdbUgM+dW7kXidZqCttBrmw7BhN4TMddkCw==
debug@2.6.9:
version "2.6.9"