This commit is contained in:
Chris Grimmett 2023-12-24 03:57:11 -08:00
parent 86b2b7625f
commit 3641c40984
23 changed files with 870 additions and 128 deletions

View File

@ -6,7 +6,7 @@ import CalHeatmap from 'cal-heatmap';
import Legend from 'cal-heatmap/plugins/Legend';
// @ts-ignore cal-heatmap is jenk
import Tooltip from 'cal-heatmap/plugins/Tooltip';
import { DataRecord } from 'cal-heatmap/src/options/Options'
import { DataRecord } from 'cal-heatmap/src/options/Options';
import 'cal-heatmap/cal-heatmap.css';
import dayjs from 'dayjs';
import { useEffect, useState, useRef } from 'react';

View File

@ -14,13 +14,6 @@ export default async function FundingGoal(): Promise<React.JSX.Element> {
const goals = await getGoals(pledgeSum);
if (!goals || !goals?.featuredFunded?.amountCents || !goals?.featuredUnfunded?.amountCents || !goals?.featuredFunded?.amountCents || !goals?.featuredUnfunded?.completedPercentage || !goals?.featuredFunded?.completedPercentage ) return <></>
// return (
// <pre>
// <code>
// {JSON.stringify(goals, null, 2)}
// </code>
// </pre>
// )
return (
<>
{/* <p>

View File

@ -5,9 +5,7 @@ interface ILocalizedDateProps {
}
export function LocalizedDate ({ date }: ILocalizedDateProps) {
return (
<span>
{new Date(date).toLocaleDateString()}
</span>
)
const isoString = date.toISOString();
const localeString = date.toLocaleDateString();
return <time dateTime={isoString}>{localeString}</time>
}

View File

@ -0,0 +1,141 @@
'use client';
import { IStream } from "@/lib/streams";
import NotFound from "app/s/[cuid]/not-found";
import { IVod } from "@/lib/vods";
import Link from "next/link";
import Image from "next/image";
import { LocalizedDate } from "./localized-date";
import { FontAwesomeIcon, FontAwesomeIconProps } from "@fortawesome/react-fontawesome";
import { faCheck, faTriangleExclamation, faCircleInfo, faThumbsUp, IconDefinition } from "@fortawesome/free-solid-svg-icons";
import styles from '@/assets/styles/fp.module.css'
import { Hemisphere, Moon } from "lunarphase-js";
import { useEffect, useState } from "react";
export interface IStreamProps {
stream: IStream;
}
type Status = 'missing' | 'issue' | 'good';
interface StyleDef {
heading: string;
icon: IconDefinition;
desc1: string;
desc2: string;
}
function capitalizeFirstLetter(string: string): string {
return string.charAt(0).toUpperCase() + string.slice(1);
}
export default function StreamPage({ stream }: IStreamProps) {
if (!stream) return <NotFound></NotFound>
const displayName = stream.attributes.vtuber.data.attributes.displayName;
const date = new Date(stream.attributes.date);
const [hemisphere, setHemisphere] = useState(Hemisphere.NORTHERN);
const [selectedStatus, setSelectedStatus] = useState<Status>('missing');
const styleMap: Record<Status, StyleDef> = {
'missing': {
heading: 'is-danger',
icon: faTriangleExclamation,
desc1: "We don't have a VOD for this stream.",
desc2: 'Know someone who does?'
},
'issue': {
heading: 'is-warning',
icon: faCircleInfo,
desc1: "We have a VOD for this stream, but it's not full quality.",
desc2: 'Have a better copy?'
},
'good': {
heading: 'is-success',
icon: faThumbsUp,
desc1: "We have a VOD for this stream, and we think it's the best quality possible.",
desc2: "Have one that's even better?"
}
};
const { heading, icon, desc1, desc2 } = styleMap[selectedStatus] || {};
useEffect(() => {
const randomHemisphere = (Math.random() < 0.5 ? 0 : 1) ? Hemisphere.NORTHERN : Hemisphere.SOUTHERN;
setHemisphere(randomHemisphere);
}, []);
return (
<>
<div className="content">
<div className="section">
<h1 className="title"><LocalizedDate date={date} /> {displayName} Stream Archive</h1>
</div>
<div className="section columns is-multiline">
<div className="column is-half">
<div className="box">
<h2 className="title is-3">Details</h2>
<div className="columns is-multiline">
<div className="column is-full">
<span><b>ISO Date</b>&nbsp;</span><span>{date.toISOString()}</span><br></br>
<span><b>Lunar Phase</b>&nbsp;</span><span>{Moon.lunarPhase(date)} {Moon.lunarPhaseEmoji(date, { hemisphere })}</span>
<br></br>
<select className="mt-5"
value={selectedStatus}
onChange={e => setSelectedStatus(e.target.value as Status)}
>
<option>good</option>
<option>issue</option>
<option>missing</option>
</select>
</div>
</div>
</div>
</div>
<div className="column is-half">
<article className={`message ${heading}`}>
<div className="message-header">
<span>VOD {capitalizeFirstLetter(selectedStatus)}</span>
</div>
<div className="message-body has-text-centered">
<span className="title is-1"><FontAwesomeIcon icon={icon}></FontAwesomeIcon></span>
<p className="mt-3">{desc1}</p>
<p className="mt-5">{desc2}<br />
<Link href={`/upload?cuid=${stream.attributes.cuid}`}>Upload it here.</Link></p>
</div>
</article>
</div>
</div>
<div className="section">
<h1 className="title">VODs</h1>
<table className="table">
<thead>
<tr>
<th><abbr title="Thumbnail">Thmb</abbr></th>
<th>Length</th>
<th>Uploader</th>
<th>Tags</th>
<th><abbr title="Timestamps">TS</abbr></th>
<th>Note</th>
</tr>
</thead>
<tbody>
{stream.attributes.vods.data.map((vod: IVod) => (
<tr>
<th>blah</th>
</tr>
))}
</tbody>
</table>
</div>
</div>
</>
)
}

View File

@ -11,7 +11,12 @@ export function Stream({ stream }: IStreamProps) {
if (!stream) return <NotFound></NotFound>
return (
<div className="box">
<h3 className="title is-3">Stream {stream.attributes.date}</h3>
<pre>
<code>
{JSON.stringify(stream, null, 2)}
</code>
</pre>
{/* <h3 className="title is-3">Stream {stream.attributes.date}</h3> */}
</div>
)
}

View File

@ -0,0 +1,68 @@
import React from 'react'
import Link from 'next/link';
import VodCard from './vod-card';
import { IVtuber } from '@/lib/vtubers';
import { IVod } from '@/lib/vods';
import { getVodTitle } from './vod-page';
import { notFound } from 'next/navigation';
import { IStream, getStreamsForVtuber } from '@/lib/streams';
interface IStreamsListProps {
vtuber: IVtuber;
page: number;
pageSize: number;
}
interface IStreamsListHeadingProps {
slug: string;
displayName: string;
}
export function StreamsListHeading({ slug, displayName }: IStreamsListHeadingProps): React.JSX.Element {
return (
<div className='box'>
<h3 className='title'>
<Link href={`/vt/${slug}`}>{displayName}</Link> Streams
</h3>
</div>
)
}
export default async function StreamsList({ vtuber, page = 1, pageSize = 24 }: IStreamsListProps): Promise<React.JSX.Element> {
if (!vtuber) return <div>vtuber is not defined. vtuber:{JSON.stringify(vtuber, null, 2)}</div>
// if (!vods) return <div>failed to load vods</div>;
const streams = await getStreamsForVtuber(vtuber.id);
if (!streams) return notFound();
// @todo [x] pagination
// @todo [x] sortability
return (
<>
{/* <p>VodsList on page {page}, pageSize {pageSize}, with {vods.data.length} vods</p> */}
{/* <pre>
<code>
{JSON.stringify(vods.data, null, 2)}
</code>
</pre> */}
<div className="columns is-multiline is-mobile">
{streams.data.map((stream: IStream) => (
<p>@todo StreamCard</p>
// <StreamCard
// key={vod.id}
// id={vod.id}
// title={getVodTitle(vod)}
// date={vod.attributes.date2}
// muxAsset={vod.attributes.muxAsset?.data?.attributes.playbackId}
// vtuber={vod.attributes.vtuber.data}
// thumbnail={vod.attributes.thumbnail?.data?.attributes?.cdnUrl}
// />
))}
</div>
</>
);
}

View File

@ -49,7 +49,7 @@ export function Tagger({ vod, setTimestamps }: ITaggerProps): React.JSX.Element
}
});
const [isEditor, setIsEditor] = useState(false);
const [tagSuggestions, setTagSuggestions] = useState<ITagSuggestion[]>([])
const [tagSuggestions, setTagSuggestions] = useState<ITagSuggestion[]>([]);
const { authData } = useAuth();
const { timeStamp, tvrs, setTvrs } = useContext(VideoContext);
const router = useRouter();
@ -149,7 +149,8 @@ export function Tagger({ vod, setTimestamps }: ITaggerProps): React.JSX.Element
}
}
if (!authData?.accessToken) return <></>
if (isEditor) {
return (
<div className='card mt-2' style={{ width: '100%' }}>
@ -225,5 +226,6 @@ export function Tagger({ vod, setTimestamps }: ITaggerProps): React.JSX.Element
</button>
);
}
}

View File

@ -16,7 +16,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faTags, faLink } from "@fortawesome/free-solid-svg-icons";
import { Tag } from './tag';
import Link from 'next/link';
import VodNav from "./vod-nav";
import VodNav from './vod-nav';
export interface IVideoInteractiveProps {

View File

@ -1,7 +1,7 @@
'use client';
import { faVideo, faExternalLinkAlt } from "@fortawesome/free-solid-svg-icons";
import { faTwitter } from '@fortawesome/free-brands-svg-icons';
import { faVideo, faExternalLinkAlt, faShareAlt } from "@fortawesome/free-solid-svg-icons";
import { faXTwitter } from '@fortawesome/free-brands-svg-icons';
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import Image from 'next/image'
import Link from 'next/link';
@ -19,7 +19,6 @@ export interface IVodNavProps {
}
export default function VodNav ({ vod, safeDate }: IVodNavProps) {
return (
<nav className='level'>
<div className='level-left'>
@ -44,7 +43,7 @@ export default function VodNav ({ vod, safeDate }: IVodNavProps) {
<div className='level-item'>
<Link
download={getDownloadLink(vod.attributes.videoSrcHash, safeDate, vod.attributes.vtuber.data.attributes.slug, 'source')}
className='button is-info is-small mb-1'
className='button is-info is-small'
target="_blank"
prefetch={false}
href={getDownloadLink(vod.attributes.videoSrcHash, safeDate, vod.attributes.vtuber.data.attributes.slug, 'source')}
@ -61,7 +60,7 @@ export default function VodNav ({ vod, safeDate }: IVodNavProps) {
<span>
<Link
download={getDownloadLink(vod.attributes.video240Hash, safeDate, vod.attributes.vtuber.data.attributes.slug, '240p')}
className='button is-info is-small mb-1'
className='button is-info is-small'
target="_blank"
prefetch={false}
href={getDownloadLink(vod.attributes.video240Hash, safeDate, vod.attributes.vtuber.data.attributes.slug, '240p')}
@ -72,16 +71,15 @@ export default function VodNav ({ vod, safeDate }: IVodNavProps) {
</Link>
</span>
</div>
)}
{vod.attributes.announceUrl && (
<div className='level-item'>
<Link
target="_blank"
href={vod.attributes.announceUrl}
className="button is-small mb-1"
className="button is-small"
>
<span className="mr-2"><FontAwesomeIcon icon={faTwitter} className="fab fa-x-twitter" /></span><span><FontAwesomeIcon icon={faExternalLinkAlt} className="fas fa-external-link-alt" /></span>
<span className="mr-2"><FontAwesomeIcon icon={faXTwitter} className="fab fa-x-twitter" /></span><span><FontAwesomeIcon icon={faExternalLinkAlt} className="fas fa-external-link-alt" /></span>
</Link>
</div>
)}

View File

@ -2,7 +2,7 @@ import Link from 'next/link';
import { getVtuberBySlug } from '../lib/vtubers'
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faExternalLinkAlt } from "@fortawesome/free-solid-svg-icons";
import { faPatreon, faTwitter, faYoutube, faTwitch, faTiktok } from "@fortawesome/free-brands-svg-icons";
import { faPatreon, faXTwitter, faYoutube, faTwitch, faTiktok } from "@fortawesome/free-brands-svg-icons";
import { faLink } from '@fortawesome/free-solid-svg-icons';
import { projektMelodyEpoch } from '@/lib/constants';
import LinkableHeading from '@/components/linkable-heading';

View File

@ -1,8 +1,8 @@
import VodsList from '@/components/vods-list'
import { IVodsResponse } from '@/lib/vods'
import Pager from '@/components/pager'
import { getVods } from '@/lib/vods'
import VodsList from '@/components/vods-list';
import { IVodsResponse } from '@/lib/vods';
import Pager from '@/components/pager';
import { getVods } from '@/lib/vods';
interface IPageParams {
params: {
@ -11,7 +11,7 @@ interface IPageParams {
}
export default async function Page({ params }: IPageParams) {
const vods: IVodsResponse = await getVods(1, 24)
const vods: IVodsResponse = await getVods(1, 24);
return (
<>

View File

@ -11,24 +11,24 @@ import NotificationCenter from './components/notification-center';
// export const metadata: Metadata = {
// title: 'Futureporn.net',
// description: "The Galaxy's Best VTuber Hentai Site",
// other: {
// RATING: 'RTA-5042-1996-1400-1577-RTA'
// },
// twitter: {
// site: '@futureporn_net',
// creator: '@cj_clippy'
// },
// alternates: {
// types: {
// 'application/atom+xml': '/feed/feed.xml',
// 'application/rss+xml': '/feed/rss.xml',
// 'application/json': '/feed/feed.json'
// }
// }
// }
export const metadata: Metadata = {
title: 'Futureporn.net',
description: "The Galaxy's Best VTuber Hentai Site",
other: {
RATING: 'RTA-5042-1996-1400-1577-RTA'
},
twitter: {
site: '@futureporn_net',
creator: '@cj_clippy'
},
alternates: {
types: {
'application/atom+xml': '/feed/feed.xml',
'application/rss+xml': '/feed/rss.xml',
'application/json': '/feed/feed.json'
}
}
}
type Props = {
children: ReactNode;

View File

@ -1,8 +1,9 @@
import { strapiUrl, siteUrl } from './constants'
import { getSafeDate } from './dates'
import { IVtuber } from './vtubers'
import qs from 'qs'
import { strapiUrl, siteUrl } from './constants';
import { getSafeDate } from './dates';
import { IVodsResponse } from './vods';
import { IVtuber, IVtuberResponse } from './vtubers';
import qs from 'qs';
export interface IStream {
@ -10,16 +11,21 @@ export interface IStream {
attributes: {
date: string;
archiveStatus: 'good' | 'issue' | 'missing';
vods: IVodsResponse;
cuid: string;
vtuber: IVtuberResponse;
}
}
export interface IStreamsResponse {
data: IStream[];
pagination: {
page: number;
pageSize: number;
pageCount: number;
total: number;
meta: {
pagination: {
page: number;
pageSize: number;
pageCount: number;
total: number;
}
}
}
@ -31,6 +37,28 @@ const fetchStreamsOptions = {
}
export async function getStreamByCuid(cuid: string): Promise<IStream> {
const query = qs.stringify({
filters: {
cuid: {
$eq: cuid
}
},
pagination: {
limit: 1
},
populate: {
vtuber: {
fields: ['slug', 'displayName', 'image', 'imageBlur'],
},
vods: '*'
}
});
const res = await fetch(`${strapiUrl}/api/streams?${query}`);
const json = await res.json();
return json.data[0];
}
export function getUrl(stream: IStream, slug: string, date: string): string {
return `${siteUrl}/vt/${slug}/stream/${getSafeDate(date)}`
}
@ -227,7 +255,7 @@ export async function getAllStreamsForVtuber(vtuberId: number, page: number = 1,
return allStreams;
}
export async function getStreamsForVtuber(vtuberId: number, page: number = 1, pageSize: number = 25, sortDesc = true): Promise<IStream> {
export async function getStreamsForVtuber(vtuberId: number, page: number = 1, pageSize: number = 25, sortDesc = true): Promise<IStreamsResponse> {
const query = qs.stringify(
{
populate: {

View File

@ -12,7 +12,7 @@ import { notFound } from "next/navigation";
export default async function Page() {
const vods = await getVods(1, 9, true);
const vtubers = await getVtubers();
if (!vtubers) notFound()
if (!vtubers) notFound();
// return (
// <pre>

View File

@ -0,0 +1,12 @@
import Link from 'next/link'
export default function NotFound() {
return (
<div className='section'>
<h2 className='title is-2'>404 Not Found</h2>
<p>Could not find that stream.</p>
<Link href="/s">Return to streams list</Link>
</div>
)
}

20
app/s/[cuid]/page.tsx Normal file
View File

@ -0,0 +1,20 @@
import StreamPage from '@/components/stream-page';
import { getStreamByCuid } from '@/lib/streams';
interface IPageParams {
params: {
cuid: string;
}
}
export default async function Page ({ params: { cuid } }: IPageParams) {
const stream = await getStreamByCuid(cuid);
return (
<>
<StreamPage stream={stream} />
</>
)
}

View File

@ -1,74 +1,70 @@
// import { getVtuberBySlug } from '@/lib/vtubers';
// import { getAllStreamsForVtuber } from '@/lib/streams';
// import NotFound from '../not-found';
// import { DataRecord } from 'cal-heatmap/src/options/Options'
// import { Cal } from '@/components/cal';
import { getVtuberBySlug } from '@/lib/vtubers';
import { getAllStreamsForVtuber } from '@/lib/streams';
import NotFound from '../not-found';
import { DataRecord } from 'cal-heatmap/src/options/Options';
import { Cal } from '@/components/cal';
// interface IPageProps {
// params: {
// slug: string;
// };
// }
interface IPageProps {
params: {
slug: string;
};
}
// function getArchiveStatusValue(archiveStatus: string): number {
// if (archiveStatus === 'good') return 2;
// if (archiveStatus === 'issue') return 1;
// else return 0 // missing
// }
function getArchiveStatusValue(archiveStatus: string): number {
if (archiveStatus === 'good') return 2;
if (archiveStatus === 'issue') return 1;
else return 0 // missing
}
// function sortDataRecordsByDate(records: DataRecord[]) {
// return records.sort((a, b) => {
// if (typeof a.date === 'string' && typeof b.date === 'string') {
// return a.date.localeCompare(b.date);
// } else {
// // Handle comparison when date is not a string (e.g., when it's a number)
// // For instance, you might want to convert numbers to strings or use a different comparison logic.
// // Example assuming number to string conversion:
// return String(a.date).localeCompare(String(b.date));
// }
// });
// }
function sortDataRecordsByDate(records: DataRecord[]) {
return records.sort((a, b) => {
if (typeof a.date === 'string' && typeof b.date === 'string') {
return a.date.localeCompare(b.date);
} else {
// Handle comparison when date is not a string (e.g., when it's a number)
// For instance, you might want to convert numbers to strings or use a different comparison logic.
// Example assuming number to string conversion:
return String(a.date).localeCompare(String(b.date));
}
});
}
// export default async function Page({ params: { slug } }: IPageProps) {
// const vtuber = await getVtuberBySlug(slug);
// if (!vtuber) return <NotFound></NotFound>
// const streams = await getAllStreamsForVtuber(vtuber.id);
// const streamsByYear: { [year: string]: DataRecord[] } = {};
// streams.forEach((stream) => {
// const date = new Date(stream.attributes.date);
// const year = date.getFullYear();
// if (!streamsByYear[year]) {
// streamsByYear[year] = [];
// }
// streamsByYear[year].push({
// date: new Date(stream.attributes.date).toISOString(),
// value: stream.attributes.archiveStatus,
// });
// });
// // Sort the data records within each year's array
// for (const year in streamsByYear) {
// streamsByYear[year] = sortDataRecordsByDate(streamsByYear[year]);
// }
export default async function Page({ params: { slug } }: IPageProps) {
const vtuber = await getVtuberBySlug(slug);
if (!vtuber) return <NotFound></NotFound>
const streams = await getAllStreamsForVtuber(vtuber.id);
const streamsByYear: { [year: string]: DataRecord[] } = {};
streams.forEach((stream) => {
const date = new Date(stream.attributes.date);
const year = date.getFullYear();
if (!streamsByYear[year]) {
streamsByYear[year] = [];
}
streamsByYear[year].push({
date: new Date(stream.attributes.date).toISOString(),
value: stream.attributes.archiveStatus,
});
});
// Sort the data records within each year's array
for (const year in streamsByYear) {
streamsByYear[year] = sortDataRecordsByDate(streamsByYear[year]);
}
// return (
// <div>
// {Object.keys(streamsByYear).map((year) => {
// return (
// <div key={year} className='section'>
// <h2 className='title'>{year}</h2>
// {/* <pre><code>{JSON.stringify(streamsByYear[year], null, 2)}</code></pre> */}
// <Cal slug={slug} data={streamsByYear[year]} />
// </div>
// )
// })}
return (
<div>
{Object.keys(streamsByYear).map((year) => {
return (
<div key={year} className='section'>
<h2 className='title'>{year}</h2>
{/* <pre><code>{JSON.stringify(streamsByYear[year], null, 2)}</code></pre> */}
<Cal slug={slug} data={streamsByYear[year]} />
</div>
)
})}
// </div>
// )
// }
export default function Page() {
return <p></p>
}
</div>
)
}

View File

@ -1,9 +1,10 @@
import VodsList from '@/components/vods-list';
import StreamsList from '@/components/streams-list';
import Link from 'next/link';
import { getVtuberBySlug } from '@/lib/vtubers'
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faExternalLinkAlt, faBagShopping } from "@fortawesome/free-solid-svg-icons";
import { faFacebook, faInstagram, faPatreon, faYoutube, faTwitch, faTiktok, faTwitter, faReddit, faDiscord } from "@fortawesome/free-brands-svg-icons";
import { faFacebook, faInstagram, faPatreon, faYoutube, faTwitch, faTiktok, faXTwitter, faReddit, faDiscord } from "@fortawesome/free-brands-svg-icons";
import Image from 'next/image'
import {
FanslyIcon,
@ -18,6 +19,7 @@ import { getVodsForVtuber } from '@/lib/vods';
import { notFound } from 'next/navigation';
export default async function Page({ params }: { params: { slug: string } }) {
const toySampleCount = 15
const vtuber = await getVtuberBySlug(params.slug);
@ -79,7 +81,7 @@ export default async function Page({ params }: { params: { slug: string } }) {
{vtuber.attributes.twitter && (
<div className="column is-3 is-narrow">
<Link target="_blank" href={vtuber.attributes.twitter} className="subtitle is-5">
<span className="mr-2"><FontAwesomeIcon icon={faTwitter} className="fab fa-x-twitter" /></span><span className="mr-2">Twitter</span><span><FontAwesomeIcon icon={faExternalLinkAlt} className="fas fa-external-link-alt" /></span>
<span className="mr-2"><FontAwesomeIcon icon={faXTwitter} className="fab fa-x-twitter" /></span><span className="mr-2">Twitter</span><span><FontAwesomeIcon icon={faExternalLinkAlt} className="fas fa-external-link-alt" /></span>
</Link>
</div>
)}
@ -206,9 +208,15 @@ export default async function Page({ params }: { params: { slug: string } }) {
</h2>
<VodsList vtuber={vtuber} vods={vods.data} page={1} pageSize={9} />
<Link className='button' href={`/vt/${vtuber.attributes.slug}/vods/1`}>See all {vtuber.attributes.displayName} vods</Link>
<Link className='button mb-5' href={`/vt/${vtuber.attributes.slug}/vods/1`}>See all {vtuber.attributes.displayName} vods</Link>
{/* <Pager getPagePath={getPaginatedUrl} slug={vtuber.slug} page={parseInt(1, 10)} pageSize={vods.pagination.pageSize} /> */}
<h2 id="streams" className='title is-3'>
<Link href="#streams">Streams</Link>
</h2>
<StreamsList vtuber={vtuber} />
<Link className='button mb-5' href={`/vt/${vtuber.attributes.slug}/streams/1`}>See all {vtuber.attributes.displayName} streams</Link>
</div>
</>
)}

View File

@ -0,0 +1,26 @@
import StreamsList, { StreamsListHeading } from '@/components/streams-list'
import { getVtuberBySlug } from '@/lib/vtubers'
import { getStreamsForVtuber } from '@/lib/streams'
import Pager from '@/components/pager'
import { notFound } from 'next/navigation'
interface IPageParams {
params: {
slug: string;
}
}
export default async function Page({ params }: IPageParams) {
const vtuber = await getVtuberBySlug(params.slug);
if (!vtuber) return <p>vtuber {params.slug} not found</p>
const streams = await getStreamsForVtuber(vtuber.id, 1, 24);
if (!streams) return <p>streams not found</p>;
return (
<>
<StreamsListHeading slug={vtuber.attributes.slug} displayName={vtuber.attributes.displayName}></StreamsListHeading>
<StreamsList vtuber={vtuber} streams={streams.data} page={1} pageSize={24} />
<Pager baseUrl={`/vt/${params.slug}/stream`} page={1} pageCount={streams.meta.pagination.pageCount} />
</>
)
}

View File

@ -0,0 +1,89 @@
$cell-height : 10px;
$cell-width : 10px;
$cell-margin:2px;
$cell-weekdays-width: 30px;
html {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}
html, body {
height: 100%;
width: 100%;
}
#container {
height: 514px;
width: 930px;
margin: 50px auto;
}
.timeline {
margin: 20px;
margin-bottom: 60px;
.timeline-months {
display: flex;
padding-left: $cell-weekdays-width;
&-month {
width: $cell-width;
margin: $cell-margin;
border: 1px solid transparent;
font-size: 10px;
}
.Jan~.Jan,
.Feb~.Feb,
.Mar~.Mar,
.Apr~.Apr,
.May~.May,
.Jun~.Jun,
.Jul~.Jul,
.Aug~.Aug,
.Sep~.Sep,
.Oct~.Oct,
.Nov~.Nov,
.Dec~.Dec {
visibility: hidden;
}
}
&-body {
display: flex;
.timeline-weekdays {
display: inline-flex;
flex-direction: column;
width: $cell-weekdays-width;
&-weekday {
font-size: 10px;
height: $cell-height;
border: 1px solid transparent;
margin: $cell-margin;
vertical-align: middle;
}
}
.timeline-cells {
display: inline-flex;
flex-direction: column;
flex-wrap: wrap;
height: #{(10 + 4) * 8}px;
&-cell {
height: $cell-height;
width: $cell-width;
border: 1px solid rgba(0, 0, 0, 0.1);
margin: $cell-margin;
border-radius: 2px;
background-color: rgba(0, 0, 0, 0.05);
&:hover {
border: 1px solid rgba(0, 0, 0, 0.3);
}
}
}
}
}

View File

@ -11,4 +11,10 @@
.isTiny {
height: 1.5em;
}
.grade {
font-family: Arial, Helvetica, sans-serif;
font-size: 8rem;
font-weight: bolder;
}

View File

@ -24,13 +24,16 @@
"@types/react-dom": "^18.2.17",
"bulma": "^0.9.4",
"bulma-prefers-dark": "0.1.0-beta.1",
"cal-heatmap": "^4.2.3",
"cid": "github:multiformats/cid",
"date-fns": "^2.30.0",
"date-fns-tz": "^2.0.0",
"dayjs": "^1.11.10",
"feed": "^4.2.2",
"gray-matter": "^4.0.3",
"hls.js": "^1.4.13",
"lodash": "^4.17.21",
"lunarphase-js": "^2.0.1",
"next": "14.0.4-canary.49",
"next-goatcounter": "^1.0.3",
"next-react-svg": "^1.2.0",

View File

@ -50,6 +50,9 @@ dependencies:
bulma-prefers-dark:
specifier: 0.1.0-beta.1
version: 0.1.0-beta.1
cal-heatmap:
specifier: ^4.2.3
version: 4.2.3
cid:
specifier: github:multiformats/cid
version: github.com/multiformats/cid/e5b6a3636d05234bc34bef873926c706afc1bd89
@ -59,6 +62,9 @@ dependencies:
date-fns-tz:
specifier: ^2.0.0
version: 2.0.0(date-fns@2.30.0)
dayjs:
specifier: ^1.11.10
version: 1.11.10
feed:
specifier: ^4.2.2
version: 4.2.2
@ -71,6 +77,9 @@ dependencies:
lodash:
specifier: ^4.17.21
version: 4.17.21
lunarphase-js:
specifier: ^2.0.1
version: 2.0.1
next:
specifier: 14.0.4-canary.49
version: 14.0.4-canary.49(react-dom@18.2.0)(react@18.2.0)(sass@1.69.5)
@ -423,6 +432,19 @@ packages:
fastq: 1.15.0
dev: true
/@observablehq/plot@0.6.13:
resolution: {integrity: sha512-ebQS4ENodOy+O3WUjhqv9jNPZENAZRQMIdO3ziOlAKfUzSf69+gaFAqqc04SGrQA6JwJjPYnbfeN3YIpNsCF/A==}
engines: {node: '>=12'}
dependencies:
d3: 7.8.5
interval-tree-1d: 1.0.4
isoformat: 0.2.1
dev: false
/@popperjs/core@2.11.8:
resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
dev: false
/@react-hookz/deep-equal@1.0.4:
resolution: {integrity: sha512-N56fTrAPUDz/R423pag+n6TXWbvlBZDtTehaGFjK0InmN+V2OFWLE/WmORhmn6Ce7dlwH5+tQN1LJFw3ngTJVg==}
dev: false
@ -753,6 +775,10 @@ packages:
engines: {node: '>=8'}
dev: false
/binary-search-bounds@2.0.5:
resolution: {integrity: sha512-H0ea4Fd3lS1+sTEB2TgcLoK21lLhwEJzlQv3IN47pJS976Gx4zoWe0ak3q+uYh60ppQxg9F16Ri4tS1sfD4+jA==}
dev: false
/bl@4.1.0:
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
dependencies:
@ -800,6 +826,20 @@ packages:
streamsearch: 1.1.0
dev: false
/cal-heatmap@4.2.3:
resolution: {integrity: sha512-NOkBb85xDjgGfP3/PPeRHp/54ookjjspzCmkPEK+amnals9xY2I3rxgnzPNYjbw5/r2xWLfFofea/VCVRQc2SA==}
dependencies:
'@observablehq/plot': 0.6.13
'@popperjs/core': 2.11.8
d3-color: 3.1.0
d3-fetch: 3.0.1
d3-selection: 3.0.0
d3-transition: 3.0.1(d3-selection@3.0.0)
dayjs: 1.11.10
eventemitter3: 5.0.1
lodash-es: 4.17.21
dev: false
/call-bind@1.0.5:
resolution: {integrity: sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==}
dependencies:
@ -885,6 +925,11 @@ packages:
color-string: 1.9.1
dev: false
/commander@7.2.0:
resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==}
engines: {node: '>= 10'}
dev: false
/concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
dev: true
@ -924,6 +969,254 @@ packages:
resolution: {integrity: sha512-N++UzR1COFip4DrtZkS99VDCcQvVCJmbqI7qaxZtHWKdAv/RUYIOnmJbP/HHk2un0PjvtFNKHfNLiFsd9BmnMQ==}
dev: false
/d3-array@3.2.4:
resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==}
engines: {node: '>=12'}
dependencies:
internmap: 2.0.3
dev: false
/d3-axis@3.0.0:
resolution: {integrity: sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==}
engines: {node: '>=12'}
dev: false
/d3-brush@3.0.0:
resolution: {integrity: sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==}
engines: {node: '>=12'}
dependencies:
d3-dispatch: 3.0.1
d3-drag: 3.0.0
d3-interpolate: 3.0.1
d3-selection: 3.0.0
d3-transition: 3.0.1(d3-selection@3.0.0)
dev: false
/d3-chord@3.0.1:
resolution: {integrity: sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==}
engines: {node: '>=12'}
dependencies:
d3-path: 3.1.0
dev: false
/d3-color@3.1.0:
resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==}
engines: {node: '>=12'}
dev: false
/d3-contour@4.0.2:
resolution: {integrity: sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==}
engines: {node: '>=12'}
dependencies:
d3-array: 3.2.4
dev: false
/d3-delaunay@6.0.4:
resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==}
engines: {node: '>=12'}
dependencies:
delaunator: 5.0.0
dev: false
/d3-dispatch@3.0.1:
resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==}
engines: {node: '>=12'}
dev: false
/d3-drag@3.0.0:
resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==}
engines: {node: '>=12'}
dependencies:
d3-dispatch: 3.0.1
d3-selection: 3.0.0
dev: false
/d3-dsv@3.0.1:
resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==}
engines: {node: '>=12'}
hasBin: true
dependencies:
commander: 7.2.0
iconv-lite: 0.6.3
rw: 1.3.3
dev: false
/d3-ease@3.0.1:
resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==}
engines: {node: '>=12'}
dev: false
/d3-fetch@3.0.1:
resolution: {integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==}
engines: {node: '>=12'}
dependencies:
d3-dsv: 3.0.1
dev: false
/d3-force@3.0.0:
resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==}
engines: {node: '>=12'}
dependencies:
d3-dispatch: 3.0.1
d3-quadtree: 3.0.1
d3-timer: 3.0.1
dev: false
/d3-format@3.1.0:
resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==}
engines: {node: '>=12'}
dev: false
/d3-geo@3.1.0:
resolution: {integrity: sha512-JEo5HxXDdDYXCaWdwLRt79y7giK8SbhZJbFWXqbRTolCHFI5jRqteLzCsq51NKbUoX0PjBVSohxrx+NoOUujYA==}
engines: {node: '>=12'}
dependencies:
d3-array: 3.2.4
dev: false
/d3-hierarchy@3.1.2:
resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==}
engines: {node: '>=12'}
dev: false
/d3-interpolate@3.0.1:
resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==}
engines: {node: '>=12'}
dependencies:
d3-color: 3.1.0
dev: false
/d3-path@3.1.0:
resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==}
engines: {node: '>=12'}
dev: false
/d3-polygon@3.0.1:
resolution: {integrity: sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==}
engines: {node: '>=12'}
dev: false
/d3-quadtree@3.0.1:
resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==}
engines: {node: '>=12'}
dev: false
/d3-random@3.0.1:
resolution: {integrity: sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==}
engines: {node: '>=12'}
dev: false
/d3-scale-chromatic@3.0.0:
resolution: {integrity: sha512-Lx9thtxAKrO2Pq6OO2Ua474opeziKr279P/TKZsMAhYyNDD3EnCffdbgeSYN5O7m2ByQsxtuP2CSDczNUIZ22g==}
engines: {node: '>=12'}
dependencies:
d3-color: 3.1.0
d3-interpolate: 3.0.1
dev: false
/d3-scale@4.0.2:
resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==}
engines: {node: '>=12'}
dependencies:
d3-array: 3.2.4
d3-format: 3.1.0
d3-interpolate: 3.0.1
d3-time: 3.1.0
d3-time-format: 4.1.0
dev: false
/d3-selection@3.0.0:
resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==}
engines: {node: '>=12'}
dev: false
/d3-shape@3.2.0:
resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==}
engines: {node: '>=12'}
dependencies:
d3-path: 3.1.0
dev: false
/d3-time-format@4.1.0:
resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==}
engines: {node: '>=12'}
dependencies:
d3-time: 3.1.0
dev: false
/d3-time@3.1.0:
resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==}
engines: {node: '>=12'}
dependencies:
d3-array: 3.2.4
dev: false
/d3-timer@3.0.1:
resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
engines: {node: '>=12'}
dev: false
/d3-transition@3.0.1(d3-selection@3.0.0):
resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==}
engines: {node: '>=12'}
peerDependencies:
d3-selection: 2 - 3
dependencies:
d3-color: 3.1.0
d3-dispatch: 3.0.1
d3-ease: 3.0.1
d3-interpolate: 3.0.1
d3-selection: 3.0.0
d3-timer: 3.0.1
dev: false
/d3-zoom@3.0.0:
resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==}
engines: {node: '>=12'}
dependencies:
d3-dispatch: 3.0.1
d3-drag: 3.0.0
d3-interpolate: 3.0.1
d3-selection: 3.0.0
d3-transition: 3.0.1(d3-selection@3.0.0)
dev: false
/d3@7.8.5:
resolution: {integrity: sha512-JgoahDG51ncUfJu6wX/1vWQEqOflgXyl4MaHqlcSruTez7yhaRKR9i8VjjcQGeS2en/jnFivXuaIMnseMMt0XA==}
engines: {node: '>=12'}
dependencies:
d3-array: 3.2.4
d3-axis: 3.0.0
d3-brush: 3.0.0
d3-chord: 3.0.1
d3-color: 3.1.0
d3-contour: 4.0.2
d3-delaunay: 6.0.4
d3-dispatch: 3.0.1
d3-drag: 3.0.0
d3-dsv: 3.0.1
d3-ease: 3.0.1
d3-fetch: 3.0.1
d3-force: 3.0.0
d3-format: 3.1.0
d3-geo: 3.1.0
d3-hierarchy: 3.1.2
d3-interpolate: 3.0.1
d3-path: 3.1.0
d3-polygon: 3.0.1
d3-quadtree: 3.0.1
d3-random: 3.0.1
d3-scale: 4.0.2
d3-scale-chromatic: 3.0.0
d3-selection: 3.0.0
d3-shape: 3.2.0
d3-time: 3.1.0
d3-time-format: 4.1.0
d3-timer: 3.0.1
d3-transition: 3.0.1(d3-selection@3.0.0)
d3-zoom: 3.0.0
dev: false
/damerau-levenshtein@1.0.8:
resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==}
dev: true
@ -943,6 +1236,10 @@ packages:
'@babel/runtime': 7.23.6
dev: false
/dayjs@1.11.10:
resolution: {integrity: sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==}
dev: false
/debug@3.2.7:
resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
peerDependencies:
@ -1004,6 +1301,12 @@ packages:
object-keys: 1.1.1
dev: true
/delaunator@5.0.0:
resolution: {integrity: sha512-AyLvtyJdbv/U1GkiS6gUUzclRoAY4Gs75qkMygJJhU75LW4DNuSF2RMzpxs9jw9Oz1BobHjTdkG3zdP55VxAqw==}
dependencies:
robust-predicates: 3.0.2
dev: false
/dequal@2.0.3:
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
engines: {node: '>=6'}
@ -1432,6 +1735,10 @@ packages:
engines: {node: '>=0.10.0'}
dev: true
/eventemitter3@5.0.1:
resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==}
dev: false
/expand-template@2.0.3:
resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==}
engines: {node: '>=6'}
@ -1709,6 +2016,13 @@ packages:
resolution: {integrity: sha512-7QGXXS0u/vu0mQqNPRBKR31ru4BLVabVSOnGaXoQZhMRNbfCNTPNJk9ToC0pzvRUfLK/71QjhQO2wdHrgbKeKg==}
dev: false
/iconv-lite@0.6.3:
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
engines: {node: '>=0.10.0'}
dependencies:
safer-buffer: 2.1.2
dev: false
/ieee754@1.2.1:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
dev: false
@ -1758,6 +2072,17 @@ packages:
side-channel: 1.0.4
dev: true
/internmap@2.0.3:
resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
engines: {node: '>=12'}
dev: false
/interval-tree-1d@1.0.4:
resolution: {integrity: sha512-wY8QJH+6wNI0uh4pDQzMvl+478Qh7Rl4qLmqiluxALlNvl+I+o5x38Pw3/z7mDPTPS1dQalZJXsmbvxx5gclhQ==}
dependencies:
binary-search-bounds: 2.0.5
dev: false
/is-array-buffer@3.0.2:
resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==}
dependencies:
@ -1933,6 +2258,10 @@ packages:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
dev: true
/isoformat@0.2.1:
resolution: {integrity: sha512-tFLRAygk9NqrRPhJSnNGh7g7oaVWDwR0wKh/GM2LgmPa50Eg4UfyaCO4I8k6EqJHl1/uh2RAD6g06n5ygEnrjQ==}
dev: false
/iterator.prototype@1.1.2:
resolution: {integrity: sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==}
dependencies:
@ -2045,6 +2374,10 @@ packages:
p-locate: 5.0.0
dev: true
/lodash-es@4.17.21:
resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
dev: false
/lodash.merge@4.6.2:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
dev: true
@ -2065,6 +2398,10 @@ packages:
dependencies:
yallist: 4.0.0
/lunarphase-js@2.0.1:
resolution: {integrity: sha512-QcCF6UxtifeSCDjbMT7FsczG4lHnWp1WRUpBy3IS5cG8pxtypk0VW/gw7xz+2vWYoaiVrt4NADZ+NR+ly4GvPg==}
dev: false
/media-chrome@1.7.0:
resolution: {integrity: sha512-2xut3GBOePwzgYGFc9+4ktWYApuL2JnMshgiw7Tgngp5tZKZugdSUGyImAgwrKM8NGE7NIcaFT584axh9dxoJw==}
dev: false
@ -2631,12 +2968,20 @@ packages:
glob: 7.2.3
dev: true
/robust-predicates@3.0.2:
resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==}
dev: false
/run-parallel@1.2.0:
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
dependencies:
queue-microtask: 1.2.3
dev: true
/rw@1.3.3:
resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==}
dev: false
/rx@4.1.0:
resolution: {integrity: sha512-CiaiuN6gapkdl+cZUr67W6I8jquN4lkak3vtIsIWCl4XIPP8ffsoyN6/+PuGXnQy8Cu8W2y9Xxh31Rq4M6wUug==}
dev: false
@ -2663,6 +3008,10 @@ packages:
is-regex: 1.1.4
dev: true
/safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
dev: false
/sass@1.69.5:
resolution: {integrity: sha512-qg2+UCJibLr2LCVOt3OlPhr/dqVHWOa9XtZf2OjbLs/T4VPSJ00udtgJxH3neXZm+QqX8B+3cU7RaLqp1iVfcQ==}
engines: {node: '>=14.0.0'}