2023-09-19 update

This commit is contained in:
Chris Grimmett 2023-09-19 19:51:05 -08:00
parent 8efc2ac422
commit 033fd2f0f4
73 changed files with 1527 additions and 13037 deletions

View File

@ -14,7 +14,7 @@ RUN pnpm add --global next
WORKDIR /app
# Copy and install the dependencies for the project
COPY package.json package-lock.json ./
COPY package.json pnpm-lock.yaml ./
# Copy all other project files to working directory
COPY . .

View File

@ -14,33 +14,48 @@ export default async function Page() {
<div className="block">
<h1>About</h1>
<div className="section hero is-primary">
<p>Futureporn is a fan-made public archive of NSFW R18 vtuber livestreams.</p>
</div>
<h1>Mission</h1>
<div className="section">
<p>It's a lofty goal, but Futureporn aims to become <b>the Galaxy's best VTuber hentai site.</b></p>
<p>It&apos;s a lofty goal, but Futureporn aims to become <b>the Galaxy&apos;s best VTuber hentai site.</b></p>
</div>
<h2>How do we get there?</h2>
<h2>How do we get there?</h2>
<h3>1. Solve the viewer's common problems</h3>
<div className="section">
<h3>1. Solve the viewer&apos;s common problems</h3>
<p>Viewers want to watch livestream VODs on their own time. Futureporn collects vods from public streams, and caches them for later viewing.</p>
<p>Viewers want to watch livestream VODs on their own time. Futureporn collects vods from public streams, and caches them for later viewing.</p>
<p>Viewers want to find content that interests them. Futureporn enables vod tagging for easy browsing.</p>
<p>Viewers want to find content that interests them. Futureporn enables vod tagging for easy browsing.</p>
</div>
<h3>2. Solve the streamer's common problems</h3>
<div className="section">
<h3>2. Solve the streamer&apos;s common problems</h3>
<p>Platforms like PH are not rising to the needs of VTubers. Instead of offering support and resources, they penalize and ban top talent.</p>
<p>Platforms like PH are not rising to the needs of VTubers. Instead of offering support and resources, they restrict and ban top talent.</p>
<p>Futureporn is different, embracing the medium and leveraging emerging technologies to amplify VTuber success.</p>
<p>Futureporn is different, embracing the medium and leveraging emerging technologies to amplify VTuber success.</p>
</div>
<h3>3. Scale beyond Earth</h3>
<div className="section">
<h3>3. Scale beyond Earth</h3>
<p>Piggybacking on <Link href="/faq#ipfs">IPFS</Link>'s potential to end 404s, VODs preserved here can withstand the test of time, and eventually exist off-world.</p>
<p>Piggybacking on <Link href="/faq#ipfs">IPFS</Link>&apos; potential to end 404s, VODs preserved here can withstand the test of time, and eventually persist <Link href="/goals">off-world</Link>.</p>
</div>
<article className="mt-5 message is-success">
<div className="message-body">
<p>Futureporn needs financial support to continue improving. If you enjoy this website, please consider <Link target="_blank" href="https://patreon.com/CJ_Clippy">becoming a patron<FontAwesomeIcon icon={faExternalLinkAlt} className="fas fa-external-link-alt" /></Link>.</p>
</div>
</article>
<div className="section">
<article className="mt-5 message is-success">
<div className="message-body">
<p>Futureporn needs financial support to continue improving. If you enjoy this website, please consider <Link target="_blank" href="https://patreon.com/CJ_Clippy">becoming a patron<FontAwesomeIcon icon={faExternalLinkAlt} className="fas fa-external-link-alt" /></Link>.</p>
</div>
</article>
</div>
</div>
</div>

10
app/api/blogs/route.ts Normal file
View File

@ -0,0 +1,10 @@
import { NextResponse } from 'next/server'
export async function GET() {
const res = await fetch('https://dummyjson.com/posts', {
next: { revalidate: 60 },
});
const data = await res.json();
return NextResponse.json(data);
}

View File

@ -15,7 +15,7 @@ export default function Page() {
<h2 className="title">RSS Feed</h2>
<p className="subtitle">Keep up to date with new VODs using Real Simple Syndication (RSS).</p>
<p>Don't have a RSS reader? Futureporn recommends <Link target="_blank" href="https://fraidyc.at/">Fraidycat <FontAwesomeIcon icon={faExternalLinkAlt} className="fas fa-external-link-alt" /></Link></p>
<p>Don&apos;t have a RSS reader? Futureporn recommends <Link target="_blank" href="https://fraidyc.at/">Fraidycat <FontAwesomeIcon icon={faExternalLinkAlt} className="fas fa-external-link-alt" /></Link></p>
<div className='field is-grouped'>
<p className='control'><a className="my-5 button is-primary" href="/feed/feed.xml">ATOM</a></p>
@ -47,7 +47,7 @@ export default function Page() {
ipfs daemon
</pre>
</li>
<li>Join the cluster <pre>CLUSTER_PEERNAME="replace-me-with-your-super-cool-name" ipfs-cluster-follow futureporn.net run --init https://futureporn.net/api/service.json</pre></li>
<li>Join the cluster <pre>CLUSTER_PEERNAME=&quot;replace-me-with-your-super-cool-name&quot; ipfs-cluster-follow futureporn.net run --init https://futureporn.net/api/service.json</pre></li>
</ol>
<h2 className="title mb-2"><a id="cluster" className="mt-3 mb-5 button is-info" href="/api/service.json">Futureporn IPFS Cluster Template (service.json)</a></h2>
<div className="mb-5"></div>

View File

@ -1,9 +1,9 @@
import { getUrl, getVods } from "@/lib/vods"
import { getURL } from "next/dist/shared/lib/utils"
export async function GET() {
const vodsRaw = await getVods()
const vods = vodsRaw.map((v) => {
const vods = vodsRaw.data.map((v) => {
return {
title: v.title,
videoSrcHash: v.videoSrcHash,

View File

@ -1,5 +1,35 @@
export default function Page() {
return (
<p>blog</p>
)
import Link from 'next/link';
import { siteUrl } from '@/lib/constants';
import { IBlogPost } from '@/lib/blog';
export default async function PostsPage() {
const res = await fetch(`${siteUrl}/api/blogs`);
const posts: IBlogPost[] = [
{
id: 1,
slug: '2021-10-29-the-story-of-futureporn',
title: 'The Story Of Futureporn'
}
]
return (
<div className="container mb-5">
<div className="content mb-5">
<h1>All Blog Posts</h1>
<hr style={{ width: '220px' }} />
<div style={{ paddingTop: '40px' }}>
{posts.map((post: IBlogPost) => (
<article key={post.slug}>
<Link href={`/blog/${post.slug}`}>
<h2>&gt; {post.title}</h2>
</Link>
</article>
))}
</div>
</div>
</div>
);
}

View File

@ -1,139 +1,131 @@
"use client";
'use client';
import { usePathname, useRouter } from 'next/navigation';
import { createContext, useContext, useState, useEffect } from 'react';
import { ReactNode } from 'react'
import { strapiUrl, patreonQuantumSupporterId, patreonSupporterBenefitId } from '../lib/constants';
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import { faPatreon } from "@fortawesome/free-brands-svg-icons";
import { useLocalStorageValue } from "@react-hookz/web";
import { faCircleXmark } from '@fortawesome/free-solid-svg-icons';
type Props = {
children?: ReactNode;
};
export interface IAuthData {
accessToken: string | null;
user: IUser | null;
}
interface IUseAuth {
authData: IAuthData;
}
interface IUser {
id: number;
username: string;
email: string;
provider: string;
confirmed: boolean;
blocked: boolean;
createdAt: string;
updatedAt: string;
isNamePublic: boolean;
avatar: string | null;
isLinkPublic: boolean;
vanityLink: string | null;
patreonBenefits: string;
}
import { createContext, useContext, ReactNode } from 'react';
import { useRouter } from 'next/navigation';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faPatreon } from '@fortawesome/free-brands-svg-icons';
import { useLocalStorageValue } from '@react-hookz/web';
import { faCircle } from '@fortawesome/free-solid-svg-icons';
import { strapiUrl } from '@/lib/constants';
import Skeleton from 'react-loading-skeleton';
export interface IJWT {
accessToken: string;
user: IUser | null;
jwt: string;
user: IUser;
}
export const AuthContext = createContext<IAuthData>({
accessToken: null,
user: null
})
export function deserializeAuthData (authData: string): IAuthData {
export interface IUser {
id: number;
username: string;
email: string;
provider: string;
confirmed: boolean;
blocked: boolean;
createdAt: string;
updatedAt: string;
isNamePublic: boolean;
avatar: string | null;
isLinkPublic: boolean;
vanityLink: string | null;
patreonBenefits: string;
}
export function AuthProvider({ children }: Props): JSX.Element {
export interface IAuthData {
accessToken: string | null;
user: IUser | null;
}
const defaultAuthData: IAuthData = {
accessToken: '',
user: null
}
const { value: authData, set: setAuthData } = useLocalStorageValue('authData', {
defaultValue: defaultAuthData,
initializeWithValue: false
})
const { value: lastVisitedPath, set: setLastVisitedPath } = useLocalStorageValue('lastVisitedPath', {
defaultValue: '/profile',
initializeWithValue: false
})
const router = useRouter()
const login = async () => {
const currentPath = window.location.pathname
setLastVisitedPath(currentPath)
router.push(`${strapiUrl}/api/connect/patreon`)
}
const logout = () => {
setAuthData({ accessToken: null, user: null })
}
export interface IUseAuth {
authData: IAuthData | null | undefined;
setAuthData: (data: IAuthData | null) => void;
lastVisitedPath: string | undefined;
login: () => void;
logout: () => void;
}
export const AuthContext = createContext<IUseAuth | null>(null);
interface IAuthContextProps {
children: ReactNode;
}
export function AuthProvider({ children }: IAuthContextProps): React.JSX.Element {
const { value: authData, set: setAuthData } = useLocalStorageValue<IAuthData | null>('authData', {
defaultValue: null,
});
return (
<AuthContext.Provider
value={{
authData,
setAuthData,
lastVisitedPath,
login,
logout
}}
>
{children}
</AuthContext.Provider>
)
};
const { value: lastVisitedPath, set: setLastVisitedPath } = useLocalStorageValue<string>('lastVisitedPath', {
defaultValue: '/profile',
initializeWithValue: false,
});
const router = useRouter();
const login = async () => {
const currentPath = window.location.pathname;
setLastVisitedPath(currentPath);
router.push(`${strapiUrl}/api/connect/patreon`);
};
const logout = () => {
setAuthData({ accessToken: null, user: null });
};
return (
<AuthContext.Provider
value={{
authData,
setAuthData,
lastVisitedPath,
login,
logout,
}}
>
{children}
</AuthContext.Provider>
);
}
export function LoginButton() {
const { login } = useAuth()
return (
<button
className="button is-primary has-icons-left"
onClick={() => {
login()
}}
>
<span className="icon is-small">
<FontAwesomeIcon icon={faPatreon} className="fab fa-patreon" />
</span>
<span>Login</span>
</button>
)
const context = useContext(AuthContext);
if (!context) return <Skeleton></Skeleton>;
const { login } = context;
return (
<button
className="button is-primary has-icons-left"
onClick={() => {
login();
}}
>
<span className="icon is-small">
<FontAwesomeIcon icon={faPatreon} className="fab fa-patreon" />
</span>
<span>Login</span>
</button>
);
}
export function LogoutButton() {
const { logout } = useAuth()
return (
<button
className="button is-secondary has-icons-left"
onClick={() => {
logout()
}}
>
<span className="icon is-small">
<FontAwesomeIcon icon={faCircleXmark} className="fas fa-circle-xmark" />
</span>
<span>Logout</span>
</button>
)
const context = useContext(AuthContext);
if (!context) return <></>;
const { logout } = context;
return (
<button
className="button is-secondary has-icons-left"
onClick={() => {
logout();
}}
>
<span className="icon is-small">
<FontAwesomeIcon icon={faCircle} className="fas fa-circle-xmark" />
</span>
<span>Logout</span>
</button>
);
}
export function useAuth(): IUseAuth {
return useContext(AuthContext);
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}

View File

@ -53,7 +53,7 @@ const useHls = (src: string, options: Options | null) => {
return { options: plyrOptions };
};
export const CustomPlyrInstance = forwardRef<
const CustomPlyrInstance = forwardRef<
APITypes,
PlyrProps & { hlsSource: string; mainColor: string; }
>((props, ref) => {
@ -64,14 +64,14 @@ export const CustomPlyrInstance = forwardRef<
}) as MutableRefObject<HTMLVideoElement>;
useEffect(() => {
if (!(ref && "current" in ref)) return;
if (!ref?.current?.plyr?.isHTML5) return;
if (!options?.previewThumbnails?.src) return;
if (!ref.current.plyr.elements) return;
ref.current.plyr.setPreviewThumbnails({
src: options?.previewThumbnails?.src,
});
}, [options]);
}, [options, ref]);
return (
<>
@ -83,3 +83,8 @@ export const CustomPlyrInstance = forwardRef<
</>
);
});
CustomPlyrInstance.displayName = 'CustomPlyrInstance'
export { CustomPlyrInstance }

View File

@ -1,6 +1,7 @@
import { getCampaign, getGoals, IGoalSample } from "../lib/patreon";
import Image from 'next/image';
import React from 'react';
export default async function FundingGoal(): Promise<React.JSX.Element> {
// const fundedGoal = patreon.goals.complete[patreon.goals.complete.length - 1];
@ -24,52 +25,52 @@ export default async function FundingGoal(): Promise<React.JSX.Element> {
const unfundedGoal = unfundedGoals.at(0) || bigHairyAudaciousGoal
return (
<>
{/* <p>this is the fundingGoal component.</p>
<p>pledgeSum:{pledgeSum}</p>
<p>fundedGoal:{JSON.stringify(fundedGoals.at(-1), null, 2)}</p>
<p>unfundedGoal:{JSON.stringify(unfundedGoals.at(0), null, 2)}</p>
<p>fundedGoal:${(fundedGoal.amountCents * (fundedGoal.completedPercentage * 0.01) / 100)} of {fundedGoal.amountCents / 100} ({fundedGoal.completedPercentage}%)</p>
<p>unfundedGoal:${(unfundedGoal.amountCents * (unfundedGoal.completedPercentage * 0.01) / 100) | 0} of ${unfundedGoal.amountCents / 100} ({unfundedGoal.completedPercentage}%)</p> */}
<article className="message is-info">
<div className="message-header">
Funding Goal
<figure className="image is-32x32 is-rounded">
<a target="_blank" href="https://twitter.com/cj_clippy">
<img className="is-rounded" src="/images/cj_clippy.jpg" alt="CJ_Clippy" />
</a>
</figure>
</div>
<div className="message-body has-text-centered">
<div className="columns">
{/* the most recently funded goal */}
<div className="column is-one-third">
<p className="subtitle">${(fundedGoal.amountCents * (fundedGoal.completedPercentage * 0.01) / 100)} of {fundedGoal.amountCents / 100} ({fundedGoal.completedPercentage}%)
</p>
<div className="mb-5 tag is-success is-rounded" style={{ width: '100%' }}>
FUNDED
</div>
<p>{fundedGoal.description}</p>
</div>
{/* the next unfunded goal */}
<div className="column is-two-thirds">
<p className="subtitle">${(unfundedGoal.amountCents * (unfundedGoal.completedPercentage * 0.01) / 100) | 0} of ${unfundedGoal.amountCents / 100} ({unfundedGoal.completedPercentage}%)</p>
<progress
className="progress is-info is-large"
value={unfundedGoal.completedPercentage}
max="100"
>
{unfundedGoal.completedPercentage}%
</progress>
<p>{unfundedGoal.description}</p>
</div>
{/* <p>this is the fundingGoal component.</p>
<p>pledgeSum:{pledgeSum}</p>
<p>fundedGoal:{JSON.stringify(fundedGoals.at(-1), null, 2)}</p>
<p>unfundedGoal:{JSON.stringify(unfundedGoals.at(0), null, 2)}</p>
<p>fundedGoal:${(fundedGoal.amountCents * (fundedGoal.completedPercentage * 0.01) / 100)} of {fundedGoal.amountCents / 100} ({fundedGoal.completedPercentage}%)</p>
<p>unfundedGoal:${(unfundedGoal.amountCents * (unfundedGoal.completedPercentage * 0.01) / 100) | 0} of ${unfundedGoal.amountCents / 100} ({unfundedGoal.completedPercentage}%)</p> */}
<article className="message is-info">
<div className="message-header">
Funding Goal
<figure className="image is-32x32 is-rounded">
<a target="_blank" href="https://twitter.com/cj_clippy">
<Image className="is-rounded" src="https://futureporn-b2.b-cdn.net/cj_clippy.jpg" alt="CJ_Clippy" fill/>
</a>
</figure>
</div>
<div className="message-body has-text-centered">
<div className="columns">
{/* the most recently funded goal */}
<div className="column is-one-third">
<p className="subtitle">${(fundedGoal.amountCents * (fundedGoal.completedPercentage * 0.01) / 100)} of {fundedGoal.amountCents / 100} ({fundedGoal.completedPercentage}%)
</p>
<div className="mb-5 tag is-success is-rounded" style={{ width: '100%' }}>
FUNDED
</div>
<p>{fundedGoal.description}</p>
</div>
<p className="mt-3 subtitle is-4">
Thank you, <a href="/patrons">Patrons!</a>
</p>
</div>
</article>
{/* the next unfunded goal */}
<div className="column is-two-thirds">
<p className="subtitle">${(unfundedGoal.amountCents * (unfundedGoal.completedPercentage * 0.01) / 100) | 0} of ${unfundedGoal.amountCents / 100} ({unfundedGoal.completedPercentage}%)</p>
<progress
className="progress is-info is-large"
value={unfundedGoal.completedPercentage}
max="100"
>
{unfundedGoal.completedPercentage}%
</progress>
<p>{unfundedGoal.description}</p>
</div>
</div>
<p className="mt-3 subtitle is-4">
Thank you, <a href="/patrons">Patrons!</a>
</p>
</div>
</article>
</>
);
};

View File

@ -7,7 +7,7 @@ import Linktree from '@/assets/svg/linktree.svg'
import Carrd from '@/assets/svg/carrd.svg'
import Anime from '@/assets/svg/noun-anime-3890912.svg'
import Avatar from '@/assets/svg/noun-avatar-3546974.svg'
import Network from '@/assets/svg/noun-network-1603820.svg'
import AdultContent from '@/assets/svg/noun-adult-content-1731184.svg'
import styles from '@/assets/styles/icon.module.css'

View File

@ -1,75 +0,0 @@
import { createHelia, Helia } from 'helia'
import { unixfs } from '@helia/unixfs'
import { CID } from 'multiformats/cid'
import React, { useState, useEffect } from 'react'
// const cid: CID = CID.parse('QmRBkKi1PnthqaBaiZnXML6fH6PNqCFdpcBxGYXoUQfp6z')
async function catFile (heliaFs) {
const textDecoder = new TextDecoder()
// for await (const data of heliaFs.cat('QmRBkKi1PnthqaBaiZnXML6fH6PNqCFdpcBxGYXoUQfp6z')) {
for await (const data of heliaFs.cat('bafybeickjad2c3w7kqgg52o27m5jbzd4uevnghpj4qqc2j2qijiomivcya')) {
console.log(textDecoder.decode(data))
}
}
const IpfsComponent = () => {
const [id, setId] = useState<CID>(null)
const [helia, setHelia] = useState<Helia|null>(null)
const [isOnline, setIsOnline] = useState<boolean>(false)
const [content, setContent] = useState<Uint8Array>(new Uint8Array());
useEffect(() => {
const init = async () => {
if (helia) return
const heliaNode = await createHelia()
const nodeId = heliaNode.libp2p.peerId.toString()
const nodeIsOnline = heliaNode.libp2p.isStarted()
setHelia(heliaNode)
setId(nodeId)
setIsOnline(nodeIsOnline)
}
init()
}, [helia])
if (!helia || !id) {
return <h4>Connecting to IPFS...</h4>
}
return (
<div>
<h4 data-test="id">ID: {id.toString()}</h4>
<h4 data-test="status">Status: {isOnline ? 'Online' : 'Offline'}</h4>
<button className="button is-primary" onClick={async () => {
console.log(`getting content from network, probably (hopefully)`)
const heliaFs = unixfs(helia)
await catFile(heliaFs)
console.log('all done')
// const content = await helia.blockstore.get(cid, {
// onProgress(evt) {
// console.info(evt)
// // console.info(`onProgress event with type=${evt.type}`)
// // console.info(JSON.stringify(evt, null, 2))
// },
// })
// console.log('finished getting cid')
// console.log(content)
// setContent(content)
}}>GET</button>
<p>content:{content} [length={content.length}]</p>
</div>
)
}
export default IpfsComponent

View File

@ -1,22 +0,0 @@
'use client';
import { useEffect } from 'react';
import { usePathname, useSearchParams } from 'next/navigation';
import NProgress from 'nprogress';
import "nprogress/nprogress.css";
export function LoadingBar() {
const pathname = usePathname()
const searchParams = useSearchParams()
useEffect(() => {
console.log(`LoadingBar useEffect was triggered`)
NProgress.done();
return () => {
NProgress.start();
};
}, [pathname, searchParams]);
return <></>
}

View File

@ -1,14 +0,0 @@
"use client";
import { SessionProvider } from "next-auth/react"
import { ReactNode } from 'react'
import { Props } from '../lib/types'
export default function NextAuthProvider ({ children }: Props): JSX.Element {
return (
<SessionProvider>
{children}
</SessionProvider>
)
};

View File

@ -1,6 +1,5 @@
import { Props } from '../lib/types'
export function DangerNotification ({ errors }: Props): JSX.Element {
export function DangerNotification ({ errors }: { errors: String[] }): JSX.Element {
return (
<div className="notification is-danger">
{errors && errors.map((error, index) => (

View File

@ -1,22 +1,18 @@
import { siteUrl } from '@/lib/constants';
import Link from 'next/link';
interface IPagerProps {
slug: string;
baseUrl: string; // Pass the base URL as a prop
page: number;
pageCount: number;
collection: string;
}
function getPagePathFactory(slug: string, collection: string): (pageNumber: number) => string {
return (pageNumber: number) => {
return `${siteUrl}/vt/${slug}/${collection}/${pageNumber}`
}
}
export default function Pager({ collection, slug, page, pageCount }: IPagerProps): React.JSX.Element {
export default function Pager({ baseUrl, page, pageCount }: IPagerProps): React.JSX.Element {
const pageNumbers = Array.from({ length: pageCount }, (_, i) => i + 1);
const getPagePath = getPagePathFactory(slug, collection)
const getPagePath = (pageNumber: number) => {
return `${baseUrl}/${pageNumber}`;
};
// Define the number of page links to show around the current page
const maxPageLinksToShow = 3;
@ -26,7 +22,6 @@ export default function Pager({ collection, slug, page, pageCount }: IPagerProps
return (
<div className="box">
{/* <p>pager slug:{slug} page:{page} pageCount:{pageCount}</p> */}
<nav className="pagination">
{page > 1 && (
<Link href={getPagePath(page - 1)} className="pagination-previous">
@ -80,7 +75,6 @@ export default function Pager({ collection, slug, page, pageCount }: IPagerProps
</li>
)}
</ul>
</nav>
</div>
);

View File

@ -1,12 +1,9 @@
import { IPatron } from '../lib/types';
import useSWR from 'swr';
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton'
import 'react-loading-skeleton/dist/skeleton.css'
import { getPatrons } from '../lib/patreon'
interface PatronsListProps {
patrons: IPatron[];
displayStyle: 'box' | 'list';
displayStyle: string;
}
export default async function PatronsList({ displayStyle }: PatronsListProps) {
@ -28,7 +25,7 @@ export default async function PatronsList({ displayStyle }: PatronsListProps) {
<div className="content">
{patron.username && (
<p>
<strong>{patron.username}</strong>
<b>{patron.username}</b>
</p>
)}
{patron.vanityLink && (
@ -51,7 +48,7 @@ export default async function PatronsList({ displayStyle }: PatronsListProps) {
const patronNames = patrons.map((patron) => patron.username.trim()).join(', ');
return <span>{patronNames}</span>;
} else {
return null; // Handle unsupported display styles or provide a default display style
return <span></span>; // Handle unsupported display styles or provide a default display style
}
}

View File

@ -1,18 +0,0 @@
import React from 'react';
import { usePlyr } from "plyr-react";
const PlyrCompo = React.forwardRef((props, ref) => {
const { source, options = null, ...rest } = props
const raptorRef = usePlyr(ref, {
source,
options,
})
return <video ref={raptorRef} className="plyr-react plyr" {...rest} />
})
export default PlyrCompo

View File

@ -1,39 +0,0 @@
import React, { useRef, useEffect } from 'react';
import { usePlyr } from 'plyr-react';
import useHls from '@/components/custom-hls-player';
import Plyr, { PlyrProps } from 'plyr-react/types';
import Hls from 'hls.js';
const Plyr2 = React.forwardRef((props: PlyrProps, ref: React.ForwardedRef<Plyr>) => {
const { sources, options } = props;
// useEffect(() => {
// const video = ref.current?.plyr?.media;
// if (!video) return;
// const hls = new Hls();
// if (props.source && props.source.src) {
// hls.loadSource(props.source.src);
// hls.attachMedia(video);
// hls.on(Hls.Events.MANIFEST_PARSED, function () {
// ref.current?.plyr?.play();
// });
// }
// }, [ref, props.source]);
return
<video
ref={usePlyr(ref, {
...useHls('https://content.jwplatform.com/manifests/vM7nH0Kl.m3u8')
})}
className="plyr-react plyr"
id="plyr"
>
</video>
});
export default Plyr2

View File

@ -1,61 +0,0 @@
export default async function SortableVodsList ({ vtuberId }: VodsListProps) {
const [filterText, setFilterText] = useState('');
const [sortOption, setSortOption] = useState('Sort');
const filteredTags = tags.filter((tag: ITag) =>
tag.name.toLowerCase().includes(filterText.toLowerCase())
);
const sortedTags = [...filteredTags].sort((a, b) => {
if (sortOption === 'Alphabetical') {
return a.name.localeCompare(b.name);
} else if (sortOption === 'Frequency') {
return b.count - a.count;
}
return 0;
});
return (
<>
<div className="field is-grouped">
<div className="control has-icons-left">
<input
className="input"
type="text"
placeholder="Filter"
value={filterText}
onChange={(e) => setFilterText(e.target.value)}
/>
<span className="icon is-small is-left">
<FontAwesomeIcon icon={faFilter} className="fas fa-filter" />
</span>
</div>
<div className="control">
<div className="select">
<select
value={sortOption}
onChange={(e) => setSortOption(e.target.value)}
>
<option>Sort</option>
<option>Alphabetical</option>
<option>Frequency</option>
</select>
</div>
</div>
</div>
<div className="tags">
{sortedTags.map((tag: ITag) => (
<span key={tag.id} className="tag">
<Link href={`/tags/${slugify(tag.name)}`} className="vod-tag">
{tag.name} ({tag.count})
</Link>
</span>
))}
</div>
</>
);
}
}

View File

@ -1,144 +1,144 @@
import { useState, useEffect } from 'react';
import slugify from 'slugify';
import { APITypes } from "plyr-react";
import { IVod } from '@/lib/vods';
import { getTimestampsForVod } from '@/lib/timestamps';
// import { useState, useEffect } from 'react';
// import slugify from 'slugify';
// import { APITypes } from "plyr-react";
// import { IVod } from '@/lib/vods';
// import { getTimestampsForVod } from '@/lib/timestamps';
interface ITaggerProps {
props: {
videoRef: APITypes,
vod: IVod
}
}
// interface ITaggerProps {
// props: {
// videoRef: APITypes,
// vod: IVod
// }
// }
const playheadTimestamp = () => {
// return Alpine.store('player').formatTime(Alpine.store('player').seconds);
};
const displayedTimestamps = () => {
return (!!selectedTag.id) ? timestamps.filter(ts => ts.tagName === selectedTag.name) : timestamps;
};
const truncateString = (str, maxLength) => {
if (str.length <= maxLength) {
return str;
}
return str.substring(0, maxLength - 1) + '…';
};
const getTagVodRelations = (page = 1) => {
setIsLoading(true);
return fetch(`${window.backend}/api/tag-vod-relations?populate=*&filters[vod][id][$eq]=${vodId}&pagination[page]=${page}`, {
method: 'GET',
headers: {
'Accept': 'application/json'
},
})
.then((res) => res.json())
.then((json) => {
const tvrs = json.data.map((tvr) => formatTagVodRelation(tvr));
setTagVodRelations((prevTVRs) => [...prevTVRs, ...tvrs]);
const totalPages = json.meta.pagination.pageCount;
const nextPage = page + 1;
if (nextPage <= totalPages) {
window.setTimeout(() => { getTagVodRelations(nextPage); }, 500);
} else {
setIsLoading(false);
}
})
.catch((e) => {
console.error(e);
setErrors((prevErrors) => [...prevErrors, 'Unable to download tag list.']);
})
.finally(() => {
setIsLoading(false);
});
};
// const formatTagVodRelation = (tvr) => {
// if (!tvr?.attributes?.tag?.data) throw new Error(`cannot format tvr id ${tvr.id} because tvr.attributes.tag.data is undefined`);
// const id = tvr.id;
// const name = tvr.attributes.tag.data.attributes.name;
// const tagId = tvr.attributes.tag.data.id;
// const vodId = tvr.attributes.vod.data.id;
// const votes = tvr.attributes.votes;
// const creatorId = tvr.attributes.creatorId;
// const createdAt = tvr.attributes.createdAt;
// const dup = false;
// return { id, name, vodId, tagId, votes, creatorId, createdAt, dup };
// const playheadTimestamp = () => {
// // return Alpine.store('player').formatTime(Alpine.store('player').seconds);
// };
const createTag = (name) => {
const data = {
name: name
};
setIsLoading(true);
fetch(`${window.backend}/api/tags`, {
method: 'POST',
body: JSON.stringify({ data }),
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${Alpine.store('auth').jwt}`
},
}).then((res) => {
setIsLoading(false);
});
};
// const displayedTimestamps = () => {
// return (!!selectedTag.id) ? timestamps.filter(ts => ts.tagName === selectedTag.name) : timestamps;
// };
// Other methods like onThumbUpTs, onThumbDownTs, voteTs, etc. should be similarly converted.
// const truncateString = (str, maxLength) => {
// if (str.length <= maxLength) {
// return str;
// }
// return str.substring(0, maxLength - 1) + '…';
// };
// const getTagVodRelations = (page = 1) => {
// setIsLoading(true);
// return fetch(`${window.backend}/api/tag-vod-relations?populate=*&filters[vod][id][$eq]=${vodId}&pagination[page]=${page}`, {
// method: 'GET',
// headers: {
// 'Accept': 'application/json'
// },
// })
// .then((res) => res.json())
// .then((json) => {
// const tvrs = json.data.map((tvr) => formatTagVodRelation(tvr));
// setTagVodRelations((prevTVRs) => [...prevTVRs, ...tvrs]);
// const totalPages = json.meta.pagination.pageCount;
// const nextPage = page + 1;
// if (nextPage <= totalPages) {
// window.setTimeout(() => { getTagVodRelations(nextPage); }, 500);
// } else {
// setIsLoading(false);
// }
// })
// .catch((e) => {
// console.error(e);
// setErrors((prevErrors) => [...prevErrors, 'Unable to download tag list.']);
// })
// .finally(() => {
// setIsLoading(false);
// });
// };
// // const formatTagVodRelation = (tvr) => {
// // if (!tvr?.attributes?.tag?.data) throw new Error(`cannot format tvr id ${tvr.id} because tvr.attributes.tag.data is undefined`);
// // const id = tvr.id;
// // const name = tvr.attributes.tag.data.attributes.name;
// // const tagId = tvr.attributes.tag.data.id;
// // const vodId = tvr.attributes.vod.data.id;
// // const votes = tvr.attributes.votes;
// // const creatorId = tvr.attributes.creatorId;
// // const createdAt = tvr.attributes.createdAt;
// // const dup = false;
// // return { id, name, vodId, tagId, votes, creatorId, createdAt, dup };
// // };
// const createTag = (name) => {
// const data = {
// name: name
// };
// setIsLoading(true);
// fetch(`${window.backend}/api/tags`, {
// method: 'POST',
// body: JSON.stringify({ data }),
// headers: {
// 'Content-Type': 'application/json',
// 'Authorization': `Bearer ${Alpine.store('auth').jwt}`
// },
// }).then((res) => {
// setIsLoading(false);
// });
// };
// // Other methods like onThumbUpTs, onThumbDownTs, voteTs, etc. should be similarly converted.
export default function Tagger({ videoRef }) {
// export default function Tagger({ videoRef }) {
const [bti, setBti] = useState(null);
const [tagVodRelations, setTagVodRelations] = useState([]);
const [tagSuggestions, setTagSuggestions] = useState([]);
const [tagsInput, setTagsInput] = useState('');
const [errors, setErrors] = useState([]);
const [selectedTag, setSelectedTag] = useState({});
const [isTsLoading, setIsTsLoading] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [isDirty, setIsDirty] = useState(false);
const [context, setContext] = useState('');
const [timestamps, setTimestamps] = useState([]);
// const [bti, setBti] = useState(null);
// const [tagVodRelations, setTagVodRelations] = useState([]);
// const [tagSuggestions, setTagSuggestions] = useState([]);
// const [tagsInput, setTagsInput] = useState('');
// const [errors, setErrors] = useState([]);
// const [selectedTag, setSelectedTag] = useState({});
// const [isTsLoading, setIsTsLoading] = useState(false);
// const [isLoading, setIsLoading] = useState(false);
// const [isDirty, setIsDirty] = useState(false);
// const [context, setContext] = useState('');
// const [timestamps, setTimestamps] = useState([]);
// useEffect(() => {
// getTimestampsForVod(vod.id)
// .then(() => {
// return getTagVodRelations();
// });
// }, []);
// // useEffect(() => {
// // getTimestampsForVod(vod.id)
// // .then(() => {
// // return getTagVodRelations();
// // });
// // }, []);
return (
<div>
<div>VOD ID: @todo</div>
<div>Playhead Timestamp: {playheadTimestamp()}</div>
{/* <div>
<input
type="text"
value={tagsInput}
onChange={(e) => setTagsInput(e.target.value)}
placeholder="Enter tags"
/>
<button onClick={() => createTag(tagsInput)}>Create Tag</button>
</div>
<div>
<ul>
{tagVodRelations.map((tvr) => (
<li key={tvr.id}>{tvr.name}</li>
))}
</ul>
</div>
<div>
<ul>
{displayedTimestamps().map((ts) => (
<li key={ts.id}>{ts.tagName}</li>
))}
</ul>
</div> */}
</div>
);
}
// return (
// <div>
// <div>VOD ID: @todo</div>
// <div>Playhead Timestamp: {playheadTimestamp()}</div>
// {/* <div>
// <input
// type="text"
// value={tagsInput}
// onChange={(e) => setTagsInput(e.target.value)}
// placeholder="Enter tags"
// />
// <button onClick={() => createTag(tagsInput)}>Create Tag</button>
// </div>
// <div>
// <ul>
// {tagVodRelations.map((tvr) => (
// <li key={tvr.id}>{tvr.name}</li>
// ))}
// </ul>
// </div>
// <div>
// <ul>
// {displayedTimestamps().map((ts) => (
// <li key={ts.id}>{ts.tagName}</li>
// ))}
// </ul>
// </div> */}
// </div>
// );
// }

View File

@ -28,7 +28,7 @@ export function ToysListHeading({ slug, displayName }: { slug: string, displayNa
return (
<div className='box'>
<h3 className='title'>
<Link href={`/vt/${slug}`}>{displayName}'s</Link> Toys
<Link href={`/vt/${slug}`}>{displayName}&apos;s</Link> Toys
</h3>
</div>
)

View File

@ -1,30 +0,0 @@
import * as React from 'react'
import videojs from 'video.js'
import 'video.js/dist/video-js.css'
export const useVideoJS = (videoJsOptions: any) => {
const videoNode = React.useRef(null)
const player = React.useRef<any>(null)
React.useEffect(() => {
player.current = videojs(videoNode.current, videoJsOptions)
return () => {
player.current.dispose()
}
}, [changedKey])
const Video = React.useCallback(
({children, ...props}) => {
return (
<div data-vjs-player key={changedKey}>
<video ref={videoNode} className="video-js" {...props}>
{children}
</video>
</div>
)
},
[changedKey],
)
return {Video, player: player.current}
}

View File

@ -5,6 +5,7 @@ import { LogoutButton, useAuth } from "../components/auth"
import { patreonQuantumSupporterId, strapiUrl } from '../lib/constants';
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faSave, faTimes, faCheck } from "@fortawesome/free-solid-svg-icons";
import Skeleton from 'react-loading-skeleton';
interface IArchiveSupporterProps {
isNamePublic: boolean;
@ -23,6 +24,8 @@ interface ISaveButtonProps {
setIsSuccess: Function;
setIsDirty: Function;
setAuthData: Function;
errors: String[];
setErrors: Function;
}
interface IQuantumSupporterProps {
@ -34,22 +37,22 @@ interface IQuantumSupporterProps {
}
export default function UserControls({ store }) {
export default function UserControls() {
const [isLoading, setIsLoading] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
const [isDirty, setIsDirty] = useState(false);
const [isNamePublic, setIsNamePublic] = useState(false);
const [isLinkPublic, setIsLinkPublic] = useState(false);
const [errors, setErrors] = useState([])
const [vanityLink, setVanityLink] = useState('')
const { authData, setAuthData } = useAuth()
if (!authData) return <p>Loading...</p>
const [vanityLink, setVanityLink] = useState(authData.user.vanityLink)
const hasUrlBenefit = authData.user.patreonBenefits.split(' ').includes(patreonQuantumSupporterId)
const hasUrlBenefit = (authData?.user?.patreonBenefits) ? authData.user.patreonBenefits.split(' ').includes(patreonQuantumSupporterId) : false;
return (
<div>
@ -81,6 +84,8 @@ export default function UserControls({ store }) {
vanityLink={vanityLink}
setVanityLink={setVanityLink}
setAuthData={setAuthData}
errors={errors}
setErrors={setErrors}
/>
</section>
</div>
@ -100,12 +105,15 @@ export function SaveButton({
vanityLink,
setVanityLink,
setAuthData,
errors,
setErrors,
}: ISaveButtonProps) {
const { authData } = useAuth();
const handleClick = async () => {
if (!authData?.user) return;
try {
setIsLoading(true);
const response = await fetch(`${strapiUrl}/api/profile/${authData.user.id}`, {
method: 'PUT',
headers: {
@ -129,14 +137,16 @@ export function SaveButton({
// Update authData if needed
const updatedAuthData = { ...authData };
if (!updatedAuthData?.user) return;
updatedAuthData.user.vanityLink = vanityLink;
updatedAuthData.user.isNamePublic = isNamePublic;
updatedAuthData.user.isLinkPublic = isLinkPublic;
setAuthData(updatedAuthData);
}
} catch (error) {
// Handle fetch or other errors
setErrors(errors.concat([error]))
if (error instanceof Error) {
setErrors(errors.concat([error.message]))
}
}
};
@ -211,7 +221,7 @@ export function ArchiveSupporterPerks({ isNamePublic, setIsNamePublic }: IArchiv
checked={isNamePublic}
onChange={() => setIsNamePublic(!isNamePublic)}
/></span>
Publicly display <b>{authData.user.username}</b> on the patrons page.
Publicly display <b>{(authData?.user?.username) ? authData.user.username : <Skeleton></Skeleton> }</b> on the patrons page.
</label>
</div>
</div>

View File

@ -1,6 +1,6 @@
'use client';
import { useEffect, useState, useRef } from 'react';
import { useEffect, useState, useRef, type RefObject } from 'react';
import Hls from "hls.js";
import { IVod } from '@/lib/vods';
import { APITypes, PlyrOptions, PlyrSource } from "plyr-react";
@ -12,6 +12,9 @@ import { buildMuxUrl, getVodTitle } from './vod-page';
import { VideoSourceSelector } from '@/components/video-source-selector'
import { buildIpfsUrl } from '@/lib/ipfs';
interface IPlyrRef {
videoRef: RefObject<APITypes>
}
interface PlayerProps {
vod: IVod;
@ -68,12 +71,12 @@ export function VideoPlayer({ vod }: PlayerProps): React.JSX.Element {
const [selectedVideoSource, setSelectedVideoSource] = useState('')
const [isEntitledToCDN, setIsEntitledToCDN] = useState(false)
const [hlsSource, setHlsSource] = useState<string>('')
const [sourceInfo, setSourceInfo] = useState<PlyrSource>(null)
const [sourceInfo, setSourceInfo] = useState<PlyrSource | null>(null)
const [isClient, setIsClient] = useState(false)
const [storyboardUrl, setStoryboardUrl] = useState<string>('https://image.mux.com/SbVSxekprYVp9owHr8OjowRAlcIk7fuXcVNGZZr006XE/storyboard.vtt?token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InFhUnd4OGQ2R2d3TnJUMDBSMzkxNHpTTkJOS1ByS2tnZmhIcFY5MDFwTUlYZyJ9.eyJleHAiOjE2OTUxODk0OTgsImF1ZCI6InMiLCJzdWIiOiJTYlZTeGVrcHJZVnA5b3dIcjhPam93UkFsY0lrN2Z1WGNWTkdaWnIwMDZYRSJ9.efNIFyBpTL11rMRITSuz_JbPcb_wo3lJeNjQtTSxn4CwR6T_x9kcgh_sVN0gQC59YfI-0QNLgnwdaXgVUEkpJrCmxoESnmLfu28Y57xv676KkQE3QKcqtpn0X88VFKA0nxEJB92topCt-9qEH3Wdl1TO4nTwNBwp_4hVTNLld1iF_W7CVNqf60Pt9p6xF1MdOnZSN7GtkZUNvBr_7dC_e-4VBxJy1c9liE-_jw6YYg8yRXwETffOXJDS_iMB-C-5Bh8QxQi-454cBEZIcczRUER3Nj0GD9HoAnvsis18Gtt8ywfl8fezlZhHFJno-1L_sy-1aXk8eKmNu462YSG2jA')
// const [plyrOptions, setPlyrOptions] = useState<PlyrOptions | null>(null)
const videoRef = useRef<APITypes>(null);
const videoRef = useRef<APITypes | null>(null);
const supported = Hls.isSupported();
useEffect(() => {
@ -106,6 +109,7 @@ export function VideoPlayer({ vod }: PlayerProps): React.JSX.Element {
}
} else if (selectedVideoSource === 'B2') {
if (!vod.videoSrcB2) return; // This shouldn't happen because videoSourceSelector won't choose B2 if there is no b2. This return is only for satisfying TS
setHlsSource(vod.videoSrcB2.cdnUrl);
setSourceInfo(buildPlyrSourceInfo(title, vod.videoSrcB2.cdnUrl))

View File

@ -54,7 +54,7 @@ export function VideoSourceSelector({
const bestSource = determineBestVideoSource();
setSelectedVideoSource(bestSource);
}
}, [isMux, isB2, isIPFS, isEntitledToCDN]);
}, [isMux, isB2, isIPFS, isEntitledToCDN, selectedVideoSource, setSelectedVideoSource]);
// Handle button click to change the selected video source
const handleSourceClick = (source: string) => {

View File

@ -1,54 +0,0 @@
import React, { useRef } from 'react';
import videojs from 'video.js';
import 'video.js/dist/video-js.css';
export const VideoJS = (props) => {
const videoRef = useRef(null);
const playerRef = useRef(null);
const {options, onReady} = props;
React.useEffect(() => {
// Make sure Video.js player is only initialized once
if (!playerRef.current) {
// The Video.js player needs to be _inside_ the component el for React 18 Strict Mode.
const videoElement = document.createElement("video-js");
videoElement.classList.add('vjs-big-play-centered');
videoRef.current.appendChild(videoElement);
const player = playerRef.current = videojs(videoElement, options, () => {
videojs.log('player is ready');
onReady && onReady(player);
});
// You could update an existing player in the `else` block here
// on prop change, for example:
} else {
const player = playerRef.current;
player.autoplay(options.autoplay);
player.src(options.sources);
}
}, [options, videoRef]);
// Dispose the Video.js player when the functional component unmounts
React.useEffect(() => {
const player = playerRef.current;
return () => {
if (player && !player.isDisposed()) {
player.dispose();
playerRef.current = null;
}
};
}, [playerRef]);
return (
<div data-vjs-player>
<div ref={videoRef} />
</div>
);
}
export default VideoJS;

View File

@ -26,7 +26,7 @@ export default function VodCard({id, title, date, muxAsset, thumbnail, vtuber}:
<div className="card-image">
<figure className="image is-16by9">
<Image
src={(thumbnail) ? thumbnail : `/images/default-thumbnail.webp`}
src={(thumbnail) ? thumbnail : `https://futureporn-b2.b-cdn.net/default-thumbnail.webp`}
alt={title}
placeholder="blur"
blurDataURL={vtuber.imageBlur}
@ -39,7 +39,7 @@ export default function VodCard({id, title, date, muxAsset, thumbnail, vtuber}:
</div>
<div className="card-content">
<h1>{title}</h1>
<LocalizedDate date={date} />
<LocalizedDate date={new Date(date)} />
<footer className="mt-3 card-footer">
<div className="card-footer-item">

View File

@ -18,7 +18,7 @@ export interface IVodNavProps {
safeDate: string;
}
export default function ({ vod, safeDate }: IVodNavProps) {
export default function VodNav ({ vod, safeDate }: IVodNavProps) {
return (
<nav className='level'>

View File

@ -1,14 +1,10 @@
import VodCard from './vod-card';
import { IVod, IVTuber } from 'lib/types';
import { getVods, getVodForDate, getUrl } from '@/lib/vods';
import { getVtuberById } from '../lib/vtubers';
import { getVodForDate } from '@/lib/vods';
import { IVod } from '@/lib/vods';
import { getDateFromSafeDate } from '@/lib/dates';
import { VideoPlayer } from './video-player';
import Link from 'next/link';
import { buildIpfsUrl } from '@/lib/ipfs'
import VodNav from '@/components/vod-nav';
import { getTagHref } from '@/lib/tags';
import { useState } from 'react';
import { ITagVodRelation } from '@/lib/tag-vod-relations';
interface PageProps {
@ -54,7 +50,7 @@ export default async function VodPage({ safeDate, slug }: PageProps) {
{vod.note && (<div className='notification'>{vod.note}</div>)}
<div className='tags'>
{vod.tagVodRelations.map((tvr: ITagVodRelation) => (
<Link href={getTagHref(tvr.tag.name)}>
<Link key={tvr.id} href={getTagHref(tvr.tag.name)}>
<span className='tag mr-1'>{tvr.tag.name}</span>
</Link>
))}

View File

@ -5,16 +5,20 @@ import { IVtuber } from '@/lib/vtubers';
import { IVods, IVod } from '@/lib/vods';
import { getVodTitle } from './vod-page';
interface VodsListProps {
vtuber: IVtuber;
interface IVodsListProps {
vtuber?: IVtuber;
vods: IVod[];
page: number;
pageSize: number;
}
interface IVodsListHeadingProps {
slug: string;
displayName: string;
}
export function VodsListHeading({ slug, displayName }: { slug: string, displayName: string }): React.JSX.Element {
export function VodsListHeading({ slug, displayName }: IVodsListHeadingProps): React.JSX.Element {
return (
<div className='box'>
<h3 className='title'>
@ -25,7 +29,7 @@ export function VodsListHeading({ slug, displayName }: { slug: string, displayNa
}
export function VodsList({ vods, page = 1, pageSize = 24 }: VodsListProps) {
export default function VodsList({ vods, page = 1, pageSize = 24 }: IVodsListProps): 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>;
@ -44,16 +48,15 @@ export function VodsList({ vods, page = 1, pageSize = 24 }: VodsListProps) {
<div className="columns is-multiline is-mobile">
{vods.map((vod: IVod) => (
<>
<VodCard
id={vod.id}
title={getVodTitle(vod)}
date={vod.date}
muxAsset={vod.muxAsset}
vtuber={vod.vtuber}
thumbnail={vod.thumbnail}
/>
</>
<VodCard
key={vod.id}
id={vod.id}
title={getVodTitle(vod)}
date={vod.date}
muxAsset={vod.muxAsset}
vtuber={vod.vtuber}
thumbnail={vod.thumbnail}
/>
))}
</div>
</>

View File

@ -1,10 +1,6 @@
import Link from "next/link";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faPatreon } from "@fortawesome/free-brands-svg-icons";
import { faVideo } from "@fortawesome/free-solid-svg-icons";
import safeDate from 'lib/safeDate';
import type { IVtuber } from '../lib/vtubers';
import { getVodsForTag, getVodsForVtuber } from "../lib/vods";
import type { IVtuber } from '@/lib/vtubers';
import { getVodsForVtuber } from "@/lib/vods";
import Image from 'next/image'
export default async function VTuberCard(props: IVtuber) {

View File

@ -1,5 +1,4 @@
import VTuberCard from './vtuber-card'
import { IVtuber } from '../lib/types'
import { getVtubers } from '../lib/vtubers'
@ -19,6 +18,9 @@ export default async function VTubers() {
displayName={vtuber.displayName}
imageBlur={vtuber.imageBlur}
image={vtuber.image}
vods={vtuber.vods}
description1={vtuber.description1}
themeColor={vtuber.themeColor}
/>
)}
</nav>

View File

@ -3,24 +3,25 @@
import { useSearchParams, useRouter } from 'next/navigation'
import Link from 'next/link'
import { useEffect, useState } from 'react'
import { strapiUrl } from '../../../lib/constants'
import { useAuth, IJWT, IAuthData, deserializeAuthData } from '../../../components/auth'
import { DangerNotification } from '../../../components/notifications'
import { strapiUrl } from '@/lib/constants'
import { useAuth, IAuthData, IUser, IJWT } from '@/components/auth'
import { DangerNotification } from '@/components/notifications'
export type AccessToken = string | null;
export default function Page() {
const searchParams = useSearchParams()
const router = useRouter()
const { authData, setAuthData, lastVisitedPath } = useAuth()
const [errors, setErrors] = useState([])
const [errors, setErrors] = useState<String[]>([])
const initAuth = async () => {
try {
const accessToken: AccessToken = getAccessTokenFromURL();
const json = await getJwt(accessToken);
if (!json) {
setErrors(errors.push('Unable to get access token from portal. Please try again later or check Futureporn Discord.'))
setErrors(errors.concat(['Unable to get access token from portal. Please try again later or check Futureporn Discord.']))
} else {
storeJwtJson(json)
redirect();
@ -32,8 +33,6 @@ export default function Page() {
const storeJwtJson = (json: IJWT) => {
console.log('storing jwt json')
console.log(json)
// Store the JWT and other relevant data in your state management system
const data: IAuthData = {

View File

@ -28,42 +28,21 @@ export default async function Page() {
<h3 id="ipfs" className='title is-5'>What is IPFS?</h3>
<p>Interplanetary File System (IPFS) is a new-ish technology which gives a unique address to every file. This address is called a Content ID, or CID for short. A CID can be used to request the file from the IPFS network.</p>
<p>IPFS is a distributed, decentralized protocol with no central point of failure. IPFS provider nodes can come and go, providing file serving capacity to the network. As long as there is at least one node pinning the content you want, you can download it.</p>
<p>There are a few ways to use IPFS. The first and easiest is to use a public gateway. These can be overloaded and slow at times, but using one is as simple as visiting a gateway URL containing the CID. One such example is <Link target="_blank" href="https://ipfs.io/ipfs/bafkreigaknpexyvxt76zgkitavbwx6ejgfheup5oybpm77f3pxzrvwpfdi"><span className='mr-1'>https://ipfs.io/ipfs/bafkreigaknpexyvxt76zgkitavbwx6ejgfheup5oybpm77f3pxzrvwpfdi</span><FontAwesomeIcon icon={faExternalLinkAlt} className="fas fa-external-link-alt" /></Link> </p>
<p>The second way is to use a web browser which supports IPFS, such as <Link href="https://brave.com/" target="_blank"><span className='mr-1'>Brave browser.</span><FontAwesomeIcon icon={faExternalLinkAlt} className="fas fa-external-link-alt" /></Link></p>
<p>Other ways include running <Link target="_blank" href="https://docs.ipfs.io/install/ipfs-desktop/"><span className="mr-1">IPFS Desktop</span><FontAwesomeIcon icon={faExternalLinkAlt} className="fas fa-external-link-alt" /></Link> on your computer, paired with <Link href="https://docs.ipfs.tech/install/ipfs-companion/"><span className='mr-1'>IPFS Companion</span><FontAwesomeIcon icon={faExternalLinkAlt} className="fas fa-external-link-alt" /></Link> browser extension.</p>
<p>There are a few ways to use IPFS, each with their own tradeoffs. Firstly, you can use a public gateway. IPFS public gateways can be overloaded and unreliable at times, but it&apos;s simple to use. All you have to do is visit a gateway URL containing the CID. One such example is <Link target="_blank" href="https://ipfs.io/ipfs/bafkreifdwhy2rnn26w5zieqxmowocxzbo7p5n7sy5u4fj7beymqoxungem"><span className='mr-1'>https://ipfs.io/ipfs/bafkreigaknpexyvxt76zgkitavbwx6ejgfheup5oybpm77f3pxzrvwpfdi</span><FontAwesomeIcon icon={faExternalLinkAlt} className="fas fa-external-link-alt" /></Link> </p>
<p>The next way to use IPFS consists of running <Link target="_blank" href="https://docs.ipfs.io/install/ipfs-desktop/"><span className="mr-1">IPFS Desktop</span><FontAwesomeIcon icon={faExternalLinkAlt} className="fas fa-external-link-alt" /></Link> on your computer. A local IPFS node runs for as long as IPFS Desktop is active, and you can query this node for the content you want. This setup works best with <Link href="https://docs.ipfs.tech/install/ipfs-companion/"><span className='mr-1'>IPFS Companion</span><FontAwesomeIcon icon={faExternalLinkAlt} className="fas fa-external-link-alt" /></Link>, or a web browser that natively supports IPFS, such as <Link href="https://brave.com/" target="_blank"><span className='mr-1'>Brave browser.</span><FontAwesomeIcon icon={faExternalLinkAlt} className="fas fa-external-link-alt" /></Link></p>
</div>
<div className="section">
<div>
<div className="mb-3">
<h3 id="not-working" className="title is-5">The videos are not working? / buffering a lot?</h3>
</div>
<p>It's not just you. Futureporn is running on a shoestring budget and cannot afford services to make every video fast.</p>
<p>If you want to watch a VOD but you can't stream it, download it first using <Link target="_blank" href="https://docs.ipfs.io/install/ipfs-desktop/"><span className="mr-1">IPFS Desktop</span><FontAwesomeIcon icon={faExternalLinkAlt} className="fas fa-external-link-alt" /></Link></p>
<Link target="_blank" href="https://www.youtube.com/video/bclXdKNEplw" className="button is-link mb-3">
<span className="icon">
<i className="fab fa-youtube"></i>
</span>
<span>IPFS Desktop video tutorial</span>
<span className="icon">
<i className="fas fa-external-link-alt"></i>
</span>
</Link>
</div>
</div>
<div className="section">
<div>
<div className="mb-3">
<h3 id="not-reachable" className="title is-5 mb-5">My browser says the video is not reachable</h3>
<h3 id="not-reachable not-working" className="title is-5 mb-5">My browser says the video is not reachable</h3>
</div>
<div>
<p>You may get an error when clicking on a video link. Errors such as <code>DNS_PROBE_FINISHED_NXDOMAIN</code></p>
<p>This is a DNS server error that occurs when a web browser isn't able to translate the domain name into an IP address.</p>
<p>This is a DNS server error that occurs when a web browser isn&apos;t able to translate the domain name into an IP address.</p>
<p>If this happens, using a different DNS server can fix it. There are many gratis services to choose from, including <Link target="_blank" href="https://cloudflare-dns.com/dns/"><span className="mr-1">Cloudflare DNS</span><FontAwesomeIcon icon={faExternalLinkAlt} className="fas fa-external-link-alt" /></Link> or <Link target="_blank" href="https://developers.google.com/speed/public-dns/"><span className="mr-1">Google DNS</span><FontAwesomeIcon icon={faExternalLinkAlt} className="fas fa-external-link-alt" /></Link>.</p>
@ -74,9 +53,9 @@ export default async function Page() {
<div className='section'>
<div className="mb-3">
<h3 id="other-luber" className="title is-5">There's a cool new Lewd Vtuber who streams on CB. Will you archive their vods?</h3>
<h3 id="other-luber" className="title is-5">There&apos;s a cool new Lewd Vtuber who streams on CB. Will you archive their vods?</h3>
</div>
<p>Yes. Futureporn aims to become the galaxy's best VTuber hentai site.</p>
<p>Yes. Futureporn aims to become the galaxy&apos;s best VTuber hentai site.</p>
</div>
<div className='section'>

View File

Before

Width:  |  Height:  |  Size: 318 B

After

Width:  |  Height:  |  Size: 318 B

View File

@ -12,7 +12,7 @@ export default async function Page() {
<p className="subtitle">Keep up to date with new VODs using Real Simple Syndication (RSS).</p>
<p>Don't have a RSS reader? Futureporn recommends <Link target="_blank" href="https://fraidyc.at/">Fraidycat <span className="icon"><i className="fas fa-external-link-alt"></i></span></Link></p>
<p>Don&apos;t have a RSS reader? Futureporn recommends <Link target="_blank" href="https://fraidyc.at/">Fraidycat <span className="icon"><i className="fas fa-external-link-alt"></i></span></Link></p>
<div className='field is-grouped'>
<p className='control'><Link className="my-5 button is-primary" href="/feed/feed.xml">ATOM</Link></p>

View File

@ -0,0 +1,33 @@
import VodsList from '@/components/vods-list';
import { getVods } from '@/lib/vods';
import Pager from '@/components/pager';
interface IPageParams {
params: {
page: number;
};
}
export default async function Page({ params: { page } }: IPageParams) {
let vods;
try {
vods = await getVods(page, 24, true);
} catch (error) {
console.error("An error occurred:", error);
return <div>Error: {JSON.stringify(error)}</div>;
}
return (
<>
<h2 className='title is-2'>Latest VODs</h2>
<p className='subtitle'>page {page}</p>
<VodsList vods={vods.data} page={page} pageSize={24} />
<Pager
baseUrl='/latest-vods'
page={page}
pageCount={vods.pagination.pageCount}
/>
</>
);
}

24
app/latest-vods/page.tsx Normal file
View File

@ -0,0 +1,24 @@
import VodsList from '@/components/vods-list'
import { IVods } from '@/lib/vods'
import Pager from '@/components/pager'
import { getVods } from '@/lib/vods'
interface IPageParams {
params: {
slug: string;
}
}
export default async function Page({ params }: IPageParams) {
const vods: IVods = await getVods(1, 24)
return (
<>
<h2 className='title is-2'>Latest VODs</h2>
<p className='subtitle'>page 1</p>
<VodsList vods={vods.data} page={1} pageSize={24} />
<Pager baseUrl='/latest-vods' page={1} pageCount={vods.pagination.pageCount} />
</>
)
}

View File

@ -6,6 +6,11 @@ import "@fortawesome/fontawesome-svg-core/styles.css";
import { AuthProvider } from './components/auth';
import type { Metadata } from 'next';
// import NextTopLoader from 'nextjs-toploader';
import { IconDescriptor } from 'next/dist/lib/metadata/types/metadata-types';
type CustomIconDescriptor = IconDescriptor & {
title: string;
};
export const metadata: Metadata = {
title: 'Futureporn.net',
@ -15,9 +20,9 @@ export const metadata: Metadata = {
},
icons: {
other: [
{ rel: 'alternate', type: 'application/atom+xml', title: 'ATOM', url: '/feed/feed.xml' },
{ rel: 'alternate', type: 'application/rss+xml', title: 'RSS', url: '/feed/rss.xml' },
{ rel: 'alternate', type: 'application/json', title: 'JSON', url: '/feed/feed.json' }
{ rel: 'alternate', type: 'application/atom+xml', title: 'ATOM', url: '/feed/feed.xml' } as CustomIconDescriptor,
{ rel: 'alternate', type: 'application/rss+xml', title: 'RSS', url: '/feed/rss.xml' } as CustomIconDescriptor,
{ rel: 'alternate', type: 'application/json', title: 'JSON', url: '/feed/feed.json' } as CustomIconDescriptor
]
}
}

View File

@ -5,10 +5,13 @@ export interface IB2File {
cdnUrl: string;
}
export function unmarshallB2File(d: any): IB2File {
console.log(`unmarshalling b2File`)
export interface IMarshalledB2File {
}
export function unmarshallB2File(d: any): IB2File | null {
console.log(d)
if (!d) return null
if (!d) return null;
return {
url: d.attributes.url,
key: d.attributes.key,

5
app/lib/blog.ts Normal file
View File

@ -0,0 +1,5 @@
export interface IBlogPost {
slug: string;
title: string;
id: number;
}

View File

@ -1,9 +1,14 @@
export type IContributor = {
export interface IContributor {
name: string;
url?: string;
id: number;
}
// export interface IMarshalledContributor {
// }
export async function getContributors(): Promise<IContributor[]> {
try {
const res = await fetch(`${process.env.NEXT_PUBLIC_STRAPI_URL}/api/contributors`);
@ -21,7 +26,7 @@ function unmarshallContributors(d: any): IContributor {
id: d.id,
name: d.attributes.name,
url: d.attributes?.url,
isFinancialDonor: d.attributes?.isFinancialDonor || false,
isVodProvider: d.attributes?.isVodProvider || false,
// isFinancialDonor: d.attributes?.isFinancialDonor || false,
// isVodProvider: d.attributes?.isVodProvider || false,
};
}

View File

@ -1,29 +0,0 @@
import { createHelia } from 'helia';
import { MemoryDatastore } from "datastore-core";
import { MemoryBlockstore } from "blockstore-core";
import { LevelDatastore } from "datastore-level";
import { LevelBlockstore } from 'blockstore-level';
import { HeliaInstanceType } from './types';
import { libp2pDefaults } from './libp2p';
let heliaInstance: HeliaInstanceType | null = null;
export default async () => {
// application-specific data lives in the datastore
const datastore = new LevelDatastore(`helia-example-datastore`);
const blockstore = new LevelBlockstore(`helia-example-blockstore`);
// const datastore = new LevelDatastore(`helia-example-datastore-${Math.random()}`);
// const blockstore = new LevelBlockstore(`helia-example-blockstore-${Math.random()}`);
if (heliaInstance != null) {
return heliaInstance;
}
// @ts-expect-error - types are borked...
heliaInstance = await createHelia({
datastore,
blockstore,
libp2p: libp2pDefaults()
});
// addToLog("Created Helia instance");
return heliaInstance;
};

View File

@ -1,166 +0,0 @@
/* eslint-disable no-console */
import React, {
useEffect,
useState,
useCallback,
createContext
} from 'react'
import { type UnixFS, unixfs } from '@helia/unixfs'
import type { Helia } from '@helia/interface'
import getHelia from '../getHelia.ts'
import { HeliaInstanceType } from '../types'
export interface HeliaContext {
helia: HeliaInstanceType | null,
fs: UnixFS | null,
error: boolean | null,
starting: boolean,
dhtMode: 'client' | 'server' | 'unknown'
status: 'Online' | 'Offline',
nodeId: string | null,
discoveredPeers: any[],
connectedPeers: any[],
multiaddrs: any[],
events: string[],
addToEventLog?: (event: string) => void
}
const defaultContextValues: Omit<HeliaContext, 'addToEventLog'> = {
helia: null,
fs: null,
error: false,
starting: true,
dhtMode: 'unknown',
status: 'Offline',
nodeId: null,
discoveredPeers: [],
connectedPeers: [],
multiaddrs: [],
events: []
}
export const HeliaContext = createContext<HeliaContext>(defaultContextValues)
export const HeliaProvider = ({ children }) => {
const [helia, setHelia] = useState<HeliaContext['helia']>(null)
const [fs, setFs] = useState<HeliaContext['fs']>(null)
const [starting, setStarting] = useState<HeliaContext['starting']>(true)
const [error, setError] = useState<HeliaContext['error']>(null)
const [dhtMode, setDhtMode] = useState<HeliaContext['dhtMode']>('unknown')
const [status, setStatus] = useState<HeliaContext['status']>('Offline')
const [nodeId, setNodeId] = useState<HeliaContext['nodeId']>(null)
const [discoveredPeers, setDiscoveredPeers] = useState<HeliaContext['discoveredPeers']>([])
const [connectedPeers, setConnectedPeers] = useState<HeliaContext['connectedPeers']>([])
const [multiaddrs, setMultiaddrs] = useState<HeliaContext['multiaddrs']>([])
const [events, setEvents] = useState<HeliaContext['events']>([])
const addToEventLog = useCallback((event: string) => {
setEvents((prev) => [...prev, event])
}, [])
const startHelia = useCallback(async () => {
if (helia) {
addToEventLog('helia already started');
} else {
}
}, [])
const onBeforeUnload = useCallback(async () => {
if (helia != null) {
addToEventLog('Stopping Helia')
await helia.stop()
}
}, [helia])
useEffect(() => {
if (helia != null) {
return
}
async function effect() {
try {
addToEventLog('Starting Helia')
const helia = await getHelia()
setHelia(helia)
setFs(unixfs(helia))
setNodeId(helia.libp2p.peerId.toString())
window.addEventListener('beforeunload', onBeforeUnload)
} catch (e) {
console.error(e)
setError(true)
}
}
effect()
return () => {
window.removeEventListener('beforeunload', onBeforeUnload)
}
}, [])
const updateInfo = useCallback(async () => {
if (helia == null) {
return
}
setDhtMode(await helia.libp2p.services.dht.getMode())
setStatus(helia.libp2p.isStarted() ? 'Online' : 'Offline')
setStarting(false)
}, [helia])
useEffect(() => {
if (helia == null || fs == null) {
return
}
(window as any).helia = helia;
(window as any).heliaFs = fs;
(window as any).discoveredPeers = discoveredPeers;
let timeoutId: any = null
const refreshFn = async () => {
updateInfo()
timeoutId = setTimeout(refreshFn, 1000)
}
refreshFn()
helia.libp2p.addEventListener("peer:discovery", (evt) => {
setDiscoveredPeers((prev) => [...prev, evt.detail])
addToEventLog(`Discovered peer ${evt.detail.id.toString()}`)
});
helia.libp2p.addEventListener("peer:connect", (evt) => {
addToEventLog(`Connected to ${evt.detail.toString()}`)
setConnectedPeers((prev) => [...prev, evt.detail])
setMultiaddrs(helia.libp2p.getMultiaddrs())
});
helia.libp2p.addEventListener("peer:disconnect", (evt) => {
addToEventLog(`Disconnected from ${evt.detail.toString()}`)
setConnectedPeers((prev) => prev.filter((peer) => peer.toString() !== evt.detail.toString()))
setMultiaddrs(helia.libp2p.getMultiaddrs())
});
// cleanup
return () => {
clearTimeout(timeoutId)
}
}, [helia, fs, updateInfo])
return (
<HeliaContext.Provider
value={{
helia,
fs,
error,
starting,
dhtMode,
status,
nodeId,
discoveredPeers,
connectedPeers,
multiaddrs,
events,
addToEventLog
}}
>{children}</HeliaContext.Provider>
)
}

View File

@ -1,78 +0,0 @@
import { gossipsub } from '@chainsafe/libp2p-gossipsub'
import { noise } from '@chainsafe/libp2p-noise'
import { yamux } from '@chainsafe/libp2p-yamux'
import { bootstrap } from '@libp2p/bootstrap'
import { ipniContentRouting } from '@libp2p/ipni-content-routing'
import { type DualKadDHT, kadDHT } from '@libp2p/kad-dht'
import { mplex } from '@libp2p/mplex'
import { webRTC, webRTCDirect } from '@libp2p/webrtc'
import { webSockets } from '@libp2p/websockets'
import { webTransport } from '@libp2p/webtransport'
// import { webTransport } from './webtransport'
import { ipnsSelector } from 'ipns/selector'
import { ipnsValidator } from 'ipns/validator'
import { autoNATService } from 'libp2p/autonat'
import { circuitRelayTransport } from 'libp2p/circuit-relay'
import { identifyService } from 'libp2p/identify'
import type { PubSub } from '@libp2p/interface-pubsub'
import type { Libp2pOptions } from 'libp2p'
import { dcutrService } from 'libp2p/dcutr'
export function libp2pDefaults (): Libp2pOptions<{ dht: DualKadDHT, pubsub: PubSub, identify: unknown, autoNAT: unknown }> {
return {
addresses: {
listen: [
'/webrtc'
]
},
transports: [
webRTC(),
webRTCDirect(),
webTransport(),
webSockets(),
circuitRelayTransport({
discoverRelays: 1
})
],
connectionEncryption: [
noise()
],
streamMuxers: [
yamux(),
mplex()
],
peerDiscovery: [
bootstrap({
list: [
'/dnsaddr/bootstrap.libp2p.io/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN',
'/dnsaddr/bootstrap.libp2p.io/p2p/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa',
'/dnsaddr/bootstrap.libp2p.io/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb',
'/dnsaddr/bootstrap.libp2p.io/p2p/QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt',
'/ip4/104.131.131.82/tcp/4001/p2p/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ'
]
})
],
contentRouters: [
ipniContentRouting('https://cid.contact')
],
services: {
// @ts-expect-error - types are borked...
dcutr: dcutrService(),
identify: identifyService(),
autoNAT: autoNATService(),
// pubsub: gossipsub(),
dht: kadDHT({
pingTimeout: 2000,
pingConcurrency: 3,
kBucketSize: 20,
clientMode: true,
validators: {
ipns: ipnsValidator
},
selectors: {
ipns: ipnsSelector
}
})
}
}
}

View File

@ -41,7 +41,7 @@ export interface IGoals {
export async function getGoals(): IGoals {
export async function getGoals(): Promise<IGoals> {
const incomplete: IIssue[] = await fetch('https://gitea.futureporn.net/api/v1/repos/futureporn/pm/issues?state=open&labels=Goal', {
next: {

View File

@ -1,8 +1,7 @@
import { authorName, authorEmail, siteUrl, title, description, siteImage, favicon, authorLink } from './constants'
import { Feed } from "feed";
import { getVods, getUrl } from '@/lib/vods'
import { getDateFromSafeDate } from './dates';
import { ITagVodRelation } from './tags';
import { getVods, getUrl, IVod } from '@/lib/vods'
import { ITagVodRelation } from '@/lib/tag-vod-relations';
export async function generateFeeds() {
const feedOptions = {
@ -30,7 +29,7 @@ export async function generateFeeds() {
const vods = await getVods()
vods.map((vod) => {
vods.data.map((vod: IVod) => {
feed.addItem({
title: vod.title || vod.announceTitle,
description: vod.title, // @todo vod.spoiler or vod.note could go here

View File

@ -1,11 +1,11 @@
import qs from 'qs';
import { strapiUrl } from './constants'
import { unmarshallTag, ITag } from './tags';
import { unmarshallTag, ITag, IToyTag } from './tags';
import { IVod } from './vods';
export interface ITagVodRelation {
id: number;
tag: ITag;
tag: ITag | IToyTag;
vod: IVod;
}
@ -21,9 +21,6 @@ export interface ITagVodRelations {
}
export function unmarshallTagVodRelation(d: any): ITagVodRelation {
console.log(`unmarshalling tvr`)
console.log(d.attributes.tag.data)
// console.log(d.attributes.tag.data.attributes.toy.data.attributes.linkTag)
return {
id: d.id,
tag: unmarshallTag(d.attributes.tag.data),

View File

@ -11,23 +11,35 @@ export function getTagHref(name: string): string {
export interface ITag {
id: number;
name: string;
toy: IToy;
count: number;
}
export function unmarshallTag(d: any): ITag {
console.log('unmarshalling tag')
console.log(d)
return {
export interface IToyTag extends ITag {
toy: IToy;
}
export function unmarshallTag(d: any): ITag | IToyTag {
const tag: ITag = {
id: d.id,
name: d.attributes.name,
toy: (d?.attributes?.toy?.data) ? unmarshallToy(d.attributes.toy.data) : null,
count: 0 // count gets updated later
};
if (d.attributes.toy && d.attributes.toy.data) {
const toy: IToy = unmarshallToy(d.attributes.toy.data);
const toyTag: IToyTag = { ...tag, toy };
return toyTag;
}
return tag;
}
export async function getTags(): Promise<ITag[]> {
const tagVodRelations = await fetchPaginatedData('/api/tag-vod-relations', 100, { 'populate[0]': 'tag', 'populate[1]': 'vod' });

View File

@ -39,8 +39,7 @@ export function unmarshallTimestamps (ts: any) {
const tnShort = truncateString(tagName, 14)
const tagId = ts.attributes.tag.data.id
const vodId = ts.attributes.vod.data.id
return { id, time, vodId, tagName, tagId, createdAt, creatorId, tnShort, downvotes, upvotes }
return { id, time, vodId, tagName, tagId, tnShort }
}

View File

@ -1,13 +1,5 @@
import { strapiUrl, siteUrl } from './constants'
import { getDateFromSafeDate, getSafeDate } from './dates'
import { unmarshallVtuber, IVtuber } from './vtubers'
import qs from 'qs'
import { ITagVodRelation, unmarshallTagVodRelation } from './tags'
import { IMuxAsset } from './types'
import { ITag, unmarshallTag } from '@/lib/tags'
import { getTagVodRelationsForVtuber } from './tag-vod-relations'
import { PageNotFoundError } from 'next/dist/shared/lib/utils'
import { ITag } from '@/lib/tags'
export interface IToys {
@ -52,17 +44,16 @@ interface IToysListProps {
// }
export function unarshallLinkTag(d: any): ITag {
console.log('unmarshalling linkTag')
console.log(d)
return {
id: d.id,
name: d.attributes?.name
name: d.attributes?.name,
count: 0
}
}
export function unmarshallToy(d: any): IToy {
console.log('unmarshalling toy')
console.log(d)
const toy = {
id: d.id,
// tags: d.attributes.tags.data.map(unmarshallTag),
@ -74,7 +65,6 @@ export function unmarshallToy(d: any): IToy {
image2: d.attributes.image2,
linkTag: unarshallLinkTag(d.attributes?.linkTag?.data),
}
console.log(toy)
return toy
}

View File

@ -2,10 +2,6 @@
export type Props = {
children?: ReactNode;
};
export interface IMuxAsset {
playbackId: string;

View File

@ -1,27 +0,0 @@
import { useEffect } from "react";
import Router from "next/router";
import useSWR from "swr";
export default function useUser({
redirectTo = "",
redirectIfFound = false,
} = {}) {
const { data: user, mutate: mutateUser } = useSWR<User>("/api/user");
useEffect(() => {
// if no redirect needed, just return (example: already on /dashboard)
// if user data not yet there (fetch in progress, logged in or not) then don't do anything yet
if (!redirectTo || !user) return;
if (
// If redirectTo is set, redirect if the user was not found.
(redirectTo && !redirectIfFound && !user?.isLoggedIn) ||
// If redirectIfFound is also set, redirect if the user was found
(redirectIfFound && user?.isLoggedIn)
) {
Router.push(redirectTo);
}
}, [user, redirectIfFound, redirectTo]);
return { user, mutateUser };
}

View File

@ -26,10 +26,10 @@ export interface IVod {
muxAsset: IMuxAsset;
thumbnail: string;
vtuber: IVtuber;
tagVodRelations: ITagVodRelation;
tagVodRelations: ITagVodRelation[];
video240Hash: string;
videoSrcHash: string;
videoSrcB2: IB2File;
videoSrcB2: IB2File | null;
announceTitle: string;
announceUrl: string;
note: string;
@ -54,8 +54,7 @@ export function getDeprecatedUrl(vod: IVod): string {
}
export function unmarshallVod(d: any): IVod {
console.log('unmarshalling vod')
console.log(d)
if (!d.attributes?.vtuber?.data) {
throw new Error("panick! vod data doesn't contain vtuber. please populate.")
}
@ -68,7 +67,7 @@ export function unmarshallVod(d: any): IVod {
playbackId: d.attributes?.muxAsset?.data?.attributes?.playbackId,
assetId: d.attributes?.muxAsset?.data?.attributes?.assetId,
},
thumbnail: d.attributes?.thumbnail?.data?.attributes?.cdnUrl || '/images/default-thumbnail.webp',
thumbnail: d.attributes?.thumbnail?.data?.attributes?.cdnUrl || 'https://futureporn-b2.b-cdn.net/default-thumbnail.webp',
vtuber: unmarshallVtuber(d.attributes?.vtuber?.data),
tagVodRelations: d.attributes?.tagVodRelations?.data.map(unmarshallTagVodRelation),
video240Hash: d.attributes?.video240Hash,
@ -83,7 +82,6 @@ export function unmarshallVod(d: any): IVod {
export async function getVodForDate(date: Date): Promise<IVod> {
const iso8601DateString = date.toISOString().split('T')[0];
console.log(`getting vod for date:${date.toISOString()}, (${iso8601DateString})`)
const query = qs.stringify(
{
filters: {
@ -134,7 +132,7 @@ export async function getVod(id: number): Promise<IVod> {
// .then((data) => data.data.map(unmarshallVod))
}
export async function getVods(sortDesc = true): Promise<IVod[]> {
export async function getVods(page: number = 1, pageSize: number = 25, sortDesc = true): Promise<IVods> {
const query = qs.stringify(
{
populate: {
@ -157,12 +155,21 @@ export async function getVods(sortDesc = true): Promise<IVod[]> {
},
sort: {
date: (sortDesc) ? 'desc' : 'asc'
},
pagination: {
pageSize: pageSize,
page: page
}
}
)
return fetch(`${strapiUrl}/api/vods?${query}`)
.then((res) => res.json())
.then((data) => data.data.map(unmarshallVod))
.then((j) => (
{
data: j.data.map(unmarshallVod),
pagination: j.meta.pagination
}
))
}
@ -259,7 +266,6 @@ export async function getProgress(vtuberSlug: string): Promise<{ complete: numbe
const data = await fetch(`${strapiUrl}/api/vods?${query}`)
.then((res) => res.json())
.then((g) => {
console.log(g)
return g
})

View File

@ -44,8 +44,7 @@ export function getUrl(slug: string): string {
export function unmarshallVtuber(d: any): IVtuber {
console.log(d)
console.log('the vods is as follows')
if (!d) {
console.error('panick! unmarshallVTuber was called with undefined data')
@ -76,12 +75,12 @@ export function unmarshallVtuber(d: any): IVtuber {
description2: d.attributes?.description2,
image: d.attributes.image,
imageBlur: d.attributes?.imageBlur,
themeColor: d.attributes.themeColor
themeColor: d.attributes.themeColor,
vods: d.attributes.vods
}
}
export async function getVtuberBySlug(slug: string): Promise<IVtuber> {
console.log(`Getting vtuber by slug:${slug}`)
const query = qs.stringify(
{
filters: {
@ -102,8 +101,7 @@ export async function getVtuberBySlug(slug: string): Promise<IVtuber> {
.then((res) => res.json())
.then((d) => {
const vtuberData = d.data[0]
console.log('>>>>>>>>>>>>>>>>>>>>>>>>>>>>> >>>>>>>>>>>>>>>>>>>>>>>>>>>>> >>>>>>>>>>>>>>>>>>>>>>>>>>>>> ')
console.log(vtuberData)
return unmarshallVtuber(vtuberData)
})
}
@ -120,7 +118,7 @@ export async function getVtubers(): Promise<IVtuber[]> {
return fetch(`${strapiUrl}/api/vtubers`)
.then((res) => res.json())
.then((data) => {
return data.data.map((d) => unmarshallVtuber(d))
return data.data.map((d: IVtuber) => unmarshallVtuber(d))
})
}

View File

@ -1,37 +1,61 @@
import VTubers from './components/vtubers'
import FundingGoal from "../app/components/funding-goal";
import { VodsList } from './components/vods-list';
import { getVods } from './lib/vods';
import FundingGoal from "@/components/funding-goal";
import VodCard from "@/components/vod-card";
import { getVodTitle } from "@/components/vod-page";
import { getVods } from '@/lib/vods';
import { IVod } from "@/lib/vods";
import { getVtubers, IVtuber } from "./lib/vtubers";
import VTuberCard from "./components/vtuber-card";
import Link from 'next/link';
export default async function Page() {
const vods = await getVods(true);
// return <>{JSON.stringify(vods, null, 2)}</>
const vods = await getVods(1, 9, true);
const vtubers = await getVtubers()
return (
<>
<div className="main">
<section className="section">
<div className="container">
<h1 className="title">
The Galaxy's Best VTuber Hentai Site
</h1>
<h2 className="subtitle">For adults only (NSFW)</h2>
</div>
</section>
<section className="section">
<div className="container">
<h1 className="title">
The Galaxy&apos;s Best VTuber Hentai Site
</h1>
<h2 className="subtitle">For adults only (NSFW)</h2>
</div>
</section>
<section>
<FundingGoal/>
</section>
<section>
<FundingGoal />
</section>
<section className="section">
<div className="container">
{/* <VTubers /> */}
<VodsList page={1} pageSize={24} vods={vods} />
</div>
</section>
<section className="section">
<h2 className="is-2 title">Latest VODs</h2>
<div className="columns is-multiline is-mobile">
{vods.data.map((vod: IVod) => (
<VodCard
key={vod.id}
id={vod.id}
title={getVodTitle(vod)}
date={vod.date}
muxAsset={vod.muxAsset}
vtuber={vod.vtuber}
thumbnail={vod.thumbnail}
/>
))}
</div>
<Link className='button' href={`/latest-vods/1`}>See all Latest Vods</Link>
</section>
<section className="section">
<h2 className="is-2 title">VTubers</h2>
<nav className="columns is-multiline">
{vtubers.map((vtuber: IVtuber) =>
<VTuberCard key={vtuber.id} {...vtuber} />
)}
</nav>
</section>
</div>
</>
);

View File

@ -1,15 +1,10 @@
'use client'
import Link from "next/link";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faPatreon } from "@fortawesome/free-brands-svg-icons";
import { useAuth, login, logout, LoginButton, LogoutButton } from "../components/auth"
import { useAuth, LoginButton } from "../components/auth"
import { useRouter } from "next/navigation";
import { useEffect } from "react";
import { patreonSupporterBenefitId } from "../lib/constants";
import UserControls from "../components/user-controls";
import Skeleton, { SkeletonTheme } from "react-loading-skeleton"
import { getContributors } from "../lib/contributors"
import { skeletonHeight, skeletonBorderRadius, skeletonBaseColor, skeletonHighlightColor } from '../lib/constants'
export default function Page() {

View File

@ -1,7 +1,7 @@
import { IVod } from '@/lib/types'
import { getVods, getVodsForTag } from '@/lib/vods'
import { getVodsForTag, IVod } from '@/lib/vods'
import VodCard from '@/components/vod-card'
import Link from 'next/link'
import { getVodTitle } from '@/components/vod-page'
export default async function Page({ params }: { params: { slug: string }}) {
const vodsList = await getVodsForTag(params.slug)
@ -14,8 +14,9 @@ export default async function Page({ params }: { params: { slug: string }}) {
<div className="columns is-multiline">
{vodsList.map((vod: IVod) => (
<VodCard
key={vod.id}
id={vod.id}
title={vod.title}
title={getVodTitle(vod)}
date={vod.date}
muxAsset={vod.muxAsset}
thumbnail={vod.thumbnail}

View File

@ -1,15 +1,15 @@
'use client'
import React, { useEffect } from 'react';
import Uppy from '@uppy/core';
import { Dashboard } from '@uppy/react';
import RemoteSources from '@uppy/remote-sources';
import AwsS3Multipart from '@uppy/aws-s3-multipart';
import '@uppy/core/dist/style.min.css';
import '@uppy/dashboard/dist/style.min.css';
// import Uppy from '@uppy/core';
// import { Dashboard } from '@uppy/react';
// import RemoteSources from '@uppy/remote-sources';
// import AwsS3Multipart from '@uppy/aws-s3-multipart';
// import '@uppy/core/dist/style.min.css';
// import '@uppy/dashboard/dist/style.min.css';
import Image from 'next/image';
const uppy = new Uppy()
// const uppy = new Uppy()
@ -21,32 +21,32 @@ const uppy = new Uppy()
// // // Authorization: `Bearer ${Alpine.store('auth').jwt}`
// // }
// })
// Dashboard,
// {
// inline: true,
// target: '#uppy-dashboard',
// theme: 'auto',
// proudlyDisplayPoweredByUppy: false,
// disableInformer: false,
// // metaFields: [
// // @todo maybe add meta fields once https://github.com/transloadit/uppy/issues/4427 is fixed
// // {
// // id: 'announceUrl',
// // name: 'Stream Announcement URL',
// // placeholder: 'this is a placeholder'
// // },
// // {
// // id: 'note',
// // name: 'Note'
// // }
// // {
// // id: 'date',
// // name: 'Stream Date (ISO 8601)',
// // placeholder: '2022-12-30'
// // },
// // ]
// }
// )
// Dashboard,
// {
// inline: true,
// target: '#uppy-dashboard',
// theme: 'auto',
// proudlyDisplayPoweredByUppy: false,
// disableInformer: false,
// // metaFields: [
// // @todo maybe add meta fields once https://github.com/transloadit/uppy/issues/4427 is fixed
// // {
// // id: 'announceUrl',
// // name: 'Stream Announcement URL',
// // placeholder: 'this is a placeholder'
// // },
// // {
// // id: 'note',
// // name: 'Note'
// // }
// // {
// // id: 'date',
// // name: 'Stream Date (ISO 8601)',
// // placeholder: '2022-12-30'
// // },
// // ]
// }
// )
// import Uppy from '@uppy/core';
// import Dashboard from '@uppy/dashboard';
@ -65,13 +65,13 @@ export default function Page() {
// })
useEffect(() => {
uppy.setOptions({
Dashboard: {
theme: 'auto'
}
})
})
// useEffect(() => {
// uppy.setOptions({
// Dashboard: {
// theme: 'auto'
// }
// })
// })
// useEffect(() => {
// uppy.setOptions({
@ -80,68 +80,68 @@ export default function Page() {
// }, [props.restrictions])
// .use(
// Dashboard,
// {
// inline: true,
// target: '#uppy-dashboard',
// theme: 'auto',
// proudlyDisplayPoweredByUppy: false,
// disableInformer: false,
// // metaFields: [
// // @todo maybe add meta fields once https://github.com/transloadit/uppy/issues/4427 is fixed
// // {
// // id: 'announceUrl',
// // name: 'Stream Announcement URL',
// // placeholder: 'this is a placeholder'
// // },
// // {
// // id: 'note',
// // name: 'Note'
// // }
// // {
// // id: 'date',
// // name: 'Stream Date (ISO 8601)',
// // placeholder: '2022-12-30'
// // },
// // ]
// }
// )
// .use(RemoteSources, {
// companionUrl: window.companionUrl,
// sources: ['Box', 'OneDrive', 'Dropbox', 'GoogleDrive', 'Url'],
// })
// .use(AwsS3Multipart, {
// limit: 6,
// companionUrl: window.companionUrl,
// companionHeaders: {
// Authorization: `Bearer ${Alpine.store('auth').jwt}`
// }
// })
// Dashboard,
// {
// inline: true,
// target: '#uppy-dashboard',
// theme: 'auto',
// proudlyDisplayPoweredByUppy: false,
// disableInformer: false,
// // metaFields: [
// // @todo maybe add meta fields once https://github.com/transloadit/uppy/issues/4427 is fixed
// // {
// // id: 'announceUrl',
// // name: 'Stream Announcement URL',
// // placeholder: 'this is a placeholder'
// // },
// // {
// // id: 'note',
// // name: 'Note'
// // }
// // {
// // id: 'date',
// // name: 'Stream Date (ISO 8601)',
// // placeholder: '2022-12-30'
// // },
// // ]
// }
// )
// .use(RemoteSources, {
// companionUrl: window.companionUrl,
// sources: ['Box', 'OneDrive', 'Dropbox', 'GoogleDrive', 'Url'],
// })
// .use(AwsS3Multipart, {
// limit: 6,
// companionUrl: window.companionUrl,
// companionHeaders: {
// Authorization: `Bearer ${Alpine.store('auth').jwt}`
// }
// })
return <article className="message is-primary mt-5">
<div className="message-header">
Upload Coming Soon
<figure className="image is-32x32 is-rounded">
<a target="_blank" href="https://twitter.com/cj_clippy">
<img className="is-rounded" src="/images/cj_clippy.jpg" alt="CJ_Clippy" />
</a>
</figure>
</div>
<div className="message-body has-text-centered">
<p>There are too many lewd VTubers out in the wild! I can't possibly archive all their vods by myself, but together we can!</p>
<p className='subtitle mt-3'>Vod uploads coming soon.</p>
</div>
</article>
return <Dashboard
uppy={uppy}
plugins={[
'Dashboard',
'AwsS3Multipart',
'RemoteSources'
]}
restrictions={{maxNumberOfFiles: 1}}
theme={'auto'}
></Dashboard>;
<div className="message-header">
Upload Coming Soon
<figure className="image is-32x32 is-rounded">
<a target="_blank" href="https://twitter.com/cj_clippy">
<Image className="is-rounded" src="https://futureporn-b2.b-cdn.net/cj_clippy.jpg" alt="CJ_Clippy" fill />
</a>
</figure>
</div>
<div className="message-body has-text-centered">
<p>Together we can archive all lewdtuber vods!</p>
<p className='subtitle mt-3'><i>Vod uploads coming soon.</i></p>
</div>
</article>
// return <Dashboard
// uppy={uppy}
// plugins={[
// 'Dashboard',
// 'AwsS3Multipart',
// 'RemoteSources'
// ]}
// restrictions={{maxNumberOfFiles: 1}}
// theme={'auto'}
// ></Dashboard>;
}
// export default function upload () {

6
app/vods/page.tsx Normal file
View File

@ -0,0 +1,6 @@
import { redirect } from 'next/navigation';
export default async function Page() {
redirect('/latest-vods/1')
}

View File

@ -1,5 +1,4 @@
import { useRouter } from 'next/navigation';
import { VodsList } from '@/components/vods-list';
import VodsList from '@/components/vods-list';
import Link from 'next/link';
import { getVtuberBySlug } from '@/lib/vtubers'
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@ -15,22 +14,26 @@ import {
LinktreeIcon,
CarrdIcon,
} from '@/components/icons';
import { getVodsForVtuber, getPaginatedUrl } from '@/lib/vods';
import Pager from '@/components/pager';
import { ToysList } from '@/components/toys';
import { IToys, getToysForVtuber } from '@/lib/toys';
import { getVodsForVtuber } from '@/lib/vods';
import { ITagVodRelations, getTagVodRelationsForVtuber } from '@/lib/tag-vod-relations';
import { IToys } from '@/lib/toys';
import { IToyTag } from '@/lib/tags';
export default async function Page({ params }: { params: { slug: string }}) {
const toySampleCount = 15
const vtuber = await getVtuberBySlug(params.slug)
const vods = await getVodsForVtuber(vtuber.id, 1, 9)
const tvrs: ITagVodRelations = await getTagVodRelationsForVtuber(vtuber.id, 1, toySampleCount)
const toys: IToys = {
data: tvrs.data.map((tvr) => tvr.tag.toy),
pagination: tvrs.pagination
}
// const tvrs: ITagVodRelations = await getTagVodRelationsForVtuber(vtuber.id, 1, toySampleCount)
// const toys: IToys = {
// data: tvrs.data.map((tvr) => {
// if (tvr.tag.toy) {
// return tvr.tag.toy as IToyTag
// } else {
// return tvr.tag.
// }
// })
// pagination: tvrs.pagination
// }
// Handle loading and error states
if (!vtuber) {

View File

@ -1,32 +1,33 @@
import { VodsList, VodsListHeading } from '@/components/vods-list'
import { getVtuberBySlug } from '@/lib/vtubers'
import { IToys, getToysForVtuber } from '@/lib/toys'
import { ToysList, ToysListHeading } from '@/components/toys'
import Pager from '@/components/pager'
// import VodsList, { VodsListHeading } from '@/components/vods-list'
// import { getVtuberBySlug } from '@/lib/vtubers'
// // import { IToys, getToysForVtuber } from '@/lib/toys'
// import { ToysList, ToysListHeading } from '@/components/toys'
// import Pager from '@/components/pager'
interface IPageParams {
params: {
name: string;
page: number;
}
}
// interface IPageParams {
// params: {
// name: string;
// page: number;
// }
// }
export default async function Page({ params }: IPageParams) {
const vtuber = await getVtuberBySlug(params.slug)
const toys: IToys = await getToysForVtuber(vtuber.id, params.page, 24)
return (
<div className='box'>
<div className="">
<ToysListHeading slug={vtuber.slug} displayName={vtuber.displayName} />
<ToysList toys={toys} pageSize={12}></ToysList>
<Pager
collection='toys'
slug={vtuber.slug}
page={parseInt(params.page, 10)}
pageCount={toys.pagination.pageCount}
/>
</div>
</div>
)
export default async function Page() {
// const vtuber = await getVtuberBySlug(params.slug)
return <p>Toys pages coming soon</p>
// const toys: IToys = await getToysForVtuber(vtuber.id, params.page, 24)
// return (
// <div className='box'>
// <div className="">
// <ToysListHeading slug={vtuber.slug} displayName={vtuber.displayName} />
// <ToysList toys={toys} pageSize={12}></ToysList>
// <Pager
// collection='toys'
// slug={vtuber.slug}
// page={parseInt(params.page, 10)}
// pageCount={toys.pagination.pageCount}
// />
// </div>
// </div>
// )
}

View File

@ -1,9 +1,9 @@
import { VodsList, VodsListHeading } from '@/components/vods-list'
import { getVtuberBySlug } from '@/lib/vtubers'
import { IToys, getToysForVtuber } from '@/lib/toys'
import { ToysList } from '@/components/toys'
import Pager from '@/components/pager'
// import VodsList, { VodsListHeading } from '@/components/vods-list'
// import { getVtuberBySlug } from '@/lib/vtubers'
// // import { IToys, getToysForVtuber } from '@/lib/toys'
// import { ToysList } from '@/components/toys'
// import Pager from '@/components/pager'
interface IPageParams {
params: {
@ -12,21 +12,22 @@ interface IPageParams {
}
export default async function Page({ params }: IPageParams) {
const vtuber = await getVtuberBySlug(params.slug)
const toys: IToys = await getToysForVtuber(vtuber.id, 1, 24)
return (
<div className='box'>
<div className="">
{/* <VodsListHeading slug={vtuber.slug} displayName={vtuber.displayName} /> */}
{/* <VodsList vtuber={vtuber} vods={vods} page={params.page} pageSize={24} /> */}
<ToysList toys={toys} pageSize={12}></ToysList>
<Pager
collection='toys'
slug={vtuber.slug}
page={parseInt(params.page, 10)}
pageCount={toys.pagination.pageCount}
/>
</div>
</div>
)
// const vtuber = await getVtuberBySlug(params.slug)
return <p>toys pages coming soon</p>
// const toys: IToys = await getToysForVtuber(vtuber.id, 1, 24)
// return (
// <div className='box'>
// <div className="">
// {/* <VodsListHeading slug={vtuber.slug} displayName={vtuber.displayName} /> */}
// {/* <VodsList vtuber={vtuber} vods={vods} page={params.page} pageSize={24} /> */}
// <ToysList toys={toys} pageSize={12}></ToysList>
// <Pager
// collection='toys'
// slug={vtuber.slug}
// page={parseInt(params.page, 10)}
// pageCount={toys.pagination.pageCount}
// />
// </div>
// </div>
// )
}

View File

@ -1,16 +1,15 @@
import React from 'react';
import Link from 'next/link';
import VodsList from '../../../components/vods-list'
import { getVtuberBySlug } from '../../../lib/vtubers'
import { redirect } from 'next/navigation';
interface IPageParams {
params: {
slug: string;
}
}
export default function Page({ params: { slug } }: IPageParams) {
redirect(`/vt/${slug}/vods`)
return <Link href={`/vt/${slug}/vods`}>See {`/vt/${slug}/vods`}</Link>
}
export default async function Page({ params }: { params: { slug: string }}) {
const vtuber = await getVtuberBySlug(params.slug)
return (
<>
<div className='box'>
<h3 className='title'>
<Link href={`/vt/${vtuber.slug}`}>{vtuber.displayName}</Link> Vods</h3>
</div>
<VodsList vtuberId={vtuber.id} />
</>
)
}

View File

@ -1,26 +1,37 @@
import { VodsList, VodsListHeading } from '@/components/vods-list'
import { getVtuberBySlug, getUrl } from '@/lib/vtubers'
import { IVods, getVodsForVtuber } from '@/lib/vods'
import { redirect } from 'next/navigation'
import Pager from '@/components/pager'
import { siteUrl } from '@/lib/constants'
import VodsList, { VodsListHeading } from '@/components/vods-list';
import { getVtuberBySlug, getUrl } from '@/lib/vtubers';
import { IVods, getVodsForVtuber } from '@/lib/vods';
import Pager from '@/components/pager';
interface IPageParams {
params: {
slug: string;
page: number;
}
params: {
slug: string;
page: number;
};
}
export default async function Page({ params }: IPageParams) {
const vtuber = await getVtuberBySlug(params.slug)
const vods: IVods = await getVodsForVtuber(vtuber.id, params.page, 24, true)
return (
<>
<VodsListHeading slug={vtuber.slug} displayName={vtuber.displayName} />
<VodsList vtuber={vtuber} vods={vods.data} page={params.page} pageSize={24} />
<Pager collection='vods' slug={vtuber.slug} page={parseInt(params.page, 10)} pageCount={vods.pagination.pageCount} getPagePath={getUrl} />
</>
)
}
let vtuber, vods;
try {
vtuber = await getVtuberBySlug(params.slug);
vods = await getVodsForVtuber(vtuber.id, params.page, 24, true);
} catch (error) {
// Handle the error here (e.g., display an error message)
console.error("An error occurred:", error);
// You might also want to return an error page or message
return <div>Error: {JSON.stringify(error)}</div>;
}
return (
<>
<VodsListHeading slug={vtuber.slug} displayName={vtuber.displayName} />
<VodsList vtuber={vtuber} vods={vods.data} page={params.page} pageSize={24} />
<Pager
baseUrl={`/vt/${params.slug}/vods`}
page={params.page}
pageCount={vods.pagination.pageCount}
/>
</>
);
}

View File

@ -1,5 +1,5 @@
import { VodsList, VodsListHeading } from '@/components/vods-list'
import VodsList, { VodsListHeading } from '@/components/vods-list'
import { getVtuberBySlug, getUrl } from '@/lib/vtubers'
import { IVods, getVodsForVtuber, getPaginatedUrl } from '@/lib/vods'
import Pager from '@/components/pager'
@ -18,7 +18,7 @@ export default async function Page({ params }: IPageParams) {
<>
<VodsListHeading slug={vtuber.slug} displayName={vtuber.displayName}></VodsListHeading>
<VodsList vtuber={vtuber} vods={vods.data} page={1} pageSize={24} />
<Pager collection="vods" slug={vtuber.slug} page={parseInt(1, 10)} pageCount={vods.pagination.pageCount} getPagePath={getPaginatedUrl} />
<Pager baseUrl={`/vt/${params.slug}/vods`} page={1} pageCount={vods.pagination.pageCount} />
</>
)
}

View File

@ -10,7 +10,7 @@ export default async function Page() {
<div className='container mt-5'>
<nav className="columns is-multiline">
{vtubers.map((vtuber: IVtuber) =>
<VTuberCard {...vtuber} />
<VTuberCard key={vtuber.id} {...vtuber} />
)}
</nav>
</div>

View File

@ -43,3 +43,8 @@ $input-shadow: none
@import "../../node_modules/bulma/bulma.sass"
@import "../../node_modules/bulma-prefers-dark/bulma-prefers-dark.sass"
a.navbar-item:active,
a.navbar-item:focus,
a.navbar-item:focus-within
background-color: hsl(0, 0%, 20%)

View File

@ -1,7 +1,7 @@
/** @type {import('next').NextConfig} */
const path = require("path");
const nextConfig = {
distDir: 'dist',
output: 'standalone',
reactStrictMode: true,
sassOptions: {
includePaths: [path.join(__dirname, "assets", "styles")],

9246
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -9,46 +9,34 @@
"lint": "next lint"
},
"dependencies": {
"@chainsafe/libp2p-gossipsub": "^10.1.0",
"@chainsafe/libp2p-noise": "^13.0.1",
"@chainsafe/libp2p-yamux": "^5.0.0",
"@fortawesome/fontawesome-free": "^6.4.2",
"@fortawesome/fontawesome-svg-core": "^6.4.2",
"@fortawesome/free-brands-svg-icons": "^6.4.2",
"@fortawesome/free-solid-svg-icons": "^6.4.2",
"@fortawesome/react-fontawesome": "^0.2.0",
"@helia/interface": "^2.0.0",
"@helia/unixfs": "^1.4.1",
"@icons-pack/react-simple-icons": "^8.0.1",
"@libp2p/bootstrap": "^9.0.5",
"@libp2p/interface-pubsub": "^4.0.1",
"@libp2p/ipni-content-routing": "^2.0.0",
"@libp2p/kad-dht": "^10.0.5",
"@libp2p/mplex": "^9.0.5",
"@libp2p/webrtc": "^3.1.9",
"@libp2p/websockets": "^7.0.5",
"@libp2p/webtransport": "^3.0.9",
"@react-hookz/web": "^23.1.0",
"@types/react": "^16.14.45",
"@uppy/aws-s3-multipart": "^3.5.3",
"@uppy/dashboard": "^3.5.1",
"@types/qs": "^6.9.8",
"@types/react": "^18.2.22",
"@types/react-dom": "^18.2.7",
"@uppy/aws-s3-multipart": "^3.6.0",
"@uppy/core": "^3.5.0",
"@uppy/dashboard": "^3.5.3",
"@uppy/drag-drop": "^3.0.3",
"@uppy/file-input": "^3.0.3",
"@uppy/progress-bar": "^3.0.3",
"@uppy/react": "^3.1.3",
"@uppy/remote-sources": "^1.0.3",
"bulma": "^0.9.4",
"bulma-prefers-dark": "0.1.0-beta.1",
"cid": "github:multiformats/cid",
"date-fns": "^2.30.0",
"date-fns-tz": "^2.0.0",
"feed": "^4.2.2",
"helia": "^2.0.1",
"hls.js": "^1.4.12",
"ipns": "^6.0.5",
"libp2p": "^0.46.9",
"next": "^13.4.19",
"next": "^13.5.1",
"next-react-svg": "^1.2.0",
"nextjs-toploader": "^1.4.2",
"nprogress": "^0.2.0",
"plyr": "^3.7.8",
"plyr-react": "^5.3.0",
"qs": "^6.11.2",
@ -56,13 +44,17 @@
"react-dom": "^18.2.0",
"react-loading-skeleton": "^3.3.1",
"react-use-downloader": "^1.2.4",
"sass": "^1.66.1",
"swr": "^2.2.1"
"sass": "^1.67.0",
"sharp": "^0.32.6",
"slugify": "^1.6.6",
"svg-react-loader": "^0.4.6",
"swr": "^2.2.2"
},
"devDependencies": {
"@types/node": "^14.18.54",
"eslint": "^8.47.0",
"@types/node": "^14.18.61",
"eslint": "^8.49.0",
"eslint-config-next": "13.4.10",
"typescript": "4.5.5"
"tsc": "^2.0.4",
"typescript": "5.2.2"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -2,9 +2,15 @@
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/components/*": ["app/components/*"],
"@/lib/*": ["app/lib/*"],
"@/assets/*": ["assets/*"]
"@/components/*": [
"app/components/*"
],
"@/lib/*": [
"app/lib/*"
],
"@/assets/*": [
"assets/*"
]
},
"target": "es5",
"lib": [
@ -34,10 +40,10 @@
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
"dist/types/**/*.ts"
"dist/types/**/*.ts",
".next/types/**/*.ts"
],
"exclude": [
"node_modules"
],
"types": ["vidstack/globals"]
]
}