update
This commit is contained in:
parent
9ad127b6e9
commit
cd10a081a7
|
@ -0,0 +1,53 @@
|
|||
'use client';
|
||||
|
||||
import { ITagVodRelation } from "@/lib/tag-vod-relations"
|
||||
import { isWithinInterval, subHours } from "date-fns";
|
||||
import { faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { AuthContext, IUseAuth } from "./auth";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import Link from 'next/link';
|
||||
import { strapiUrl } from "@/lib/constants";
|
||||
|
||||
export interface ITagParams {
|
||||
tvr: ITagVodRelation;
|
||||
}
|
||||
|
||||
|
||||
function isCreatedByMeRecently(userId: number | undefined, tvr: ITagVodRelation) {
|
||||
if (!userId) return false;
|
||||
if (userId !== tvr.creatorId) return false;
|
||||
const last24H: Interval = { start: subHours(new Date(), 24), end: new Date() };
|
||||
if (!isWithinInterval(new Date(tvr.createdAt), last24H)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
async function handleDelete(authContext: IUseAuth | null, tvr: ITagVodRelation) {
|
||||
if (!authContext) return;
|
||||
const { authData } = authContext;
|
||||
const res = await fetch(`${strapiUrl}/api/tag-vod-relations/deleteMine/${tvr.id}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${authData?.accessToken}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
if (!res.ok) throw new Error(res.statusText)
|
||||
return true
|
||||
}
|
||||
|
||||
export function Tag({ tvr }: ITagParams) {
|
||||
const authContext = useContext(AuthContext);
|
||||
const [shouldRenderDeleteButton, setShouldRenderDeleteButton] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
setShouldRenderDeleteButton(isCreatedByMeRecently(authContext?.authData?.user?.id, tvr));
|
||||
}, [authContext?.authData?.user?.id, tvr]);
|
||||
|
||||
return (
|
||||
<span className="tags mr-2 mb-0" key={tvr.id}>
|
||||
<span className="tag">{tvr.tag.name}</span>
|
||||
{shouldRenderDeleteButton && <a onClick={() => handleDelete(authContext, tvr)} className="tag is-danger"><span className="icon is-small"><FontAwesomeIcon className="fas fa-trash" icon={faTrash}></FontAwesomeIcon></span></a>}
|
||||
</span>
|
||||
)
|
||||
}
|
|
@ -11,9 +11,10 @@ import { debounce } from 'lodash';
|
|||
import { strapiUrl } from '@/lib/constants';
|
||||
import { VideoContext } from './video-context';
|
||||
import { useForm } from "react-hook-form";
|
||||
import { assertTimestamp } from '@/lib/timestamps';
|
||||
import { createTimestamp } from '@/lib/timestamps';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import styles from '@/assets/styles/fp.module.css'
|
||||
import qs from 'qs';
|
||||
|
||||
interface ITaggerProps {
|
||||
vod: IVod;
|
||||
|
@ -80,8 +81,20 @@ export function Tagger({ vod }: ITaggerProps): React.JSX.Element {
|
|||
}
|
||||
|
||||
async function search(value: string) {
|
||||
const query = qs.stringify(
|
||||
{
|
||||
filters: {
|
||||
tags: {
|
||||
publishedAt: {
|
||||
$notNull: true
|
||||
}
|
||||
}
|
||||
},
|
||||
query: value
|
||||
}
|
||||
)
|
||||
if (!value) return;
|
||||
const res = await fetch(`${strapiUrl}/api/fuzzy-search/search?query=${value}`, {
|
||||
const res = await fetch(`${strapiUrl}/api/fuzzy-search/search?${query}`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${authData?.accessToken}`
|
||||
}
|
||||
|
@ -101,12 +114,15 @@ export function Tagger({ vod }: ITaggerProps): React.JSX.Element {
|
|||
async function onError(errors: IErrorsObject) {
|
||||
console.error('submit handler encoutnered an error')
|
||||
console.error(errors)
|
||||
setError('root.serverError', {
|
||||
type: 'idk',
|
||||
message: 'there was an Error '
|
||||
})
|
||||
}
|
||||
|
||||
async function onSubmit(values: { tagName: string, isTimestamp: boolean }) {
|
||||
if (!authData) throw new Error('Must be logged in');
|
||||
try {
|
||||
console.log('asserting tvr')
|
||||
const tvr = await createTagAndTvr(setError, authData, values.tagName, vod.id);
|
||||
if (!tvr) {
|
||||
setError('root.serverError', {
|
||||
|
@ -115,14 +131,10 @@ export function Tagger({ vod }: ITaggerProps): React.JSX.Element {
|
|||
})
|
||||
return;
|
||||
}
|
||||
console.log('assertint timestamp')
|
||||
if (values.isTimestamp) await assertTimestamp(setError, authData, tvr.tag.id, vod.id, timeStamp);
|
||||
console.log("htere whas no werror becauzse we are ahere")
|
||||
if (values.isTimestamp) await createTimestamp(setError, authData, tvr.tag.id, vod.id, timeStamp);
|
||||
setValue('tagName', '');
|
||||
router.refresh();
|
||||
} catch (e) {
|
||||
console.error('there was an error')
|
||||
console.error(e)
|
||||
setError('root.serverError', {
|
||||
type: 'idk'
|
||||
});
|
||||
|
|
|
@ -1,41 +1,78 @@
|
|||
'use client';
|
||||
|
||||
import React, { useContext } from "react"
|
||||
import React, { useContext, useState, useEffect, useCallback } from "react";
|
||||
import { IVod } from "@/lib/vods";
|
||||
import { IRawTimestamp, getRawTimestampsForVod } from "@/lib/timestamps";
|
||||
import { useEffect, useState } from "react";
|
||||
import { formatTimestamp, formatUrlTimestamp, parseUrlTimestamp } from "@/lib/dates";
|
||||
import { VideoContext } from "./video-context";
|
||||
import {
|
||||
IRawTimestamp,
|
||||
getRawTimestampsForVod,
|
||||
deleteTimestamp
|
||||
} from "@/lib/timestamps";
|
||||
import {
|
||||
formatTimestamp,
|
||||
formatUrlTimestamp,
|
||||
} from "@/lib/dates";
|
||||
import Link from 'next/link';
|
||||
import { faClock, faLink } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faClock, faLink, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { ITagVodRelation } from "@/lib/tag-vod-relations";
|
||||
import { AuthContext, IAuthData, IUseAuth } from "./auth";
|
||||
import pThrottle from 'p-throttle';
|
||||
import { isWithinInterval, subHours, type Interval } from 'date-fns'
|
||||
import { useRouter } from 'next/navigation';
|
||||
import styles from '@/assets/styles/fp.module.css'
|
||||
import { AppRouterInstance } from "next/dist/shared/lib/app-router-context.shared-runtime";
|
||||
|
||||
export interface ITimestampsProps {
|
||||
vod: IVod;
|
||||
selectedTag: string;
|
||||
}
|
||||
|
||||
function isCreatedByMeRecently(authData: IAuthData, ts: IRawTimestamp) {
|
||||
if (!authData?.user) return false;
|
||||
if (authData.user.id !== ts.attributes.creatorId) return false;
|
||||
const last24H: Interval = { start: subHours(new Date(), 24), end: new Date() };
|
||||
if (!isWithinInterval(new Date(ts.attributes.createdAt), last24H)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function Timestamps({ vod }: ITimestampsProps): React.JSX.Element {
|
||||
const { tvrs, setTvrs } = useContext(VideoContext);
|
||||
const [timestamps, setTimestamps] = useState<IRawTimestamp[]>([])
|
||||
const router = useRouter();
|
||||
|
||||
const throttle = pThrottle({
|
||||
limit: 1,
|
||||
interval: 1000
|
||||
});
|
||||
const throttledTimestampFetch = throttle(getRawTimestampsForVod)
|
||||
const [timestamps, setTimestamps] = useState<IRawTimestamp[]>([]);
|
||||
const authContext = useContext(AuthContext);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
getRawTimestampsForVod(vod.id).then((data) => {
|
||||
setTimestamps(data.data)
|
||||
})
|
||||
}, [vod])
|
||||
const fetchData = async (page: number) => {
|
||||
try {
|
||||
const data = await throttledTimestampFetch(vod.id, page, 25)
|
||||
setTimestamps((prevTimestamps) => [...prevTimestamps, ...data.data.filter((timestamp) => !prevTimestamps.map((t) => t.id).includes(timestamp.id))]);
|
||||
|
||||
// Fetch the next page if needed
|
||||
if (page < data.meta.pagination.pageCount) {
|
||||
fetchData(page + 1);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
};
|
||||
fetchData(1)
|
||||
}, [vod]);
|
||||
|
||||
|
||||
return (
|
||||
<div className='timestamps mb-5'>
|
||||
<h4 className='subtitle is-4'>
|
||||
<div className="timestamps mb-5">
|
||||
<h4 className="subtitle is-4">
|
||||
<a id="timestamps"></a>
|
||||
<Link href="#timestamps">
|
||||
<FontAwesomeIcon
|
||||
icon={faLink}
|
||||
className="fab fa-link mr-2"
|
||||
></FontAwesomeIcon>
|
||||
<FontAwesomeIcon
|
||||
icon={faLink}
|
||||
className="fab fa-link mr-2"
|
||||
></FontAwesomeIcon>
|
||||
</Link>
|
||||
<FontAwesomeIcon
|
||||
icon={faClock}
|
||||
|
@ -43,13 +80,37 @@ export function Timestamps({ vod }: ITimestampsProps): React.JSX.Element {
|
|||
></FontAwesomeIcon>
|
||||
<span>Timestamps</span>
|
||||
</h4>
|
||||
{(!!timestamps) ? (
|
||||
|
||||
{(timestamps.length > 0) && (
|
||||
timestamps.map((ts: IRawTimestamp) => {
|
||||
return <p key={ts.id}><Link href={`?t=${formatUrlTimestamp(ts.attributes.time)}`}>{formatTimestamp(ts.attributes.time)}</Link> {ts.attributes.tag.data.attributes.name}</p>
|
||||
return (
|
||||
// <p>{JSON.stringify(ts, null, 2)}</p>
|
||||
<p key={ts.id}>
|
||||
<Link
|
||||
href={`?t=${formatUrlTimestamp(ts.attributes.time)}`}
|
||||
className="mr-2"
|
||||
>
|
||||
{formatTimestamp(ts.attributes.time)}
|
||||
</Link>
|
||||
<span className="mr-2">{ts.attributes.tag.data.attributes.name}</span>
|
||||
{(authContext?.authData && isCreatedByMeRecently(authContext.authData, ts)) && (
|
||||
<button
|
||||
onClick={() => {
|
||||
if (!authContext?.authData) return;
|
||||
deleteTimestamp(authContext.authData, ts.id);
|
||||
setTimestamps((prevTimestamps) => prevTimestamps.filter((timestamp) => timestamp.id !== ts.id));
|
||||
}}
|
||||
className={`button is-danger icon`}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash}></FontAwesomeIcon>
|
||||
</button>
|
||||
)}
|
||||
</p>
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<p>This VOD has no timestamps</p>
|
||||
)}
|
||||
|
||||
{(timestamps.length < 1) && <p><i>This VOD has no timestamps</i></p>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,8 +15,10 @@ import { parseUrlTimestamp } from "@/lib/dates";
|
|||
import { TagButton } from '@/components/tag-button';
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faTags, faLink } from "@fortawesome/free-solid-svg-icons";
|
||||
import { Tag } from './tag';
|
||||
import Link from 'next/link';
|
||||
|
||||
|
||||
export interface IVideoInteractiveProps {
|
||||
vod: IVod;
|
||||
safeDate: string;
|
||||
|
@ -60,10 +62,10 @@ export function VideoInteractive({ vod, safeDate, localizedDate }: IVideoInterac
|
|||
const [timeStamp, setTimeStamp] = useState(0);
|
||||
const [tvrs, setTvrs] = useState([]);
|
||||
const [isPlayerReady, setIsPlayerReady] = useState(false);
|
||||
const [selectedTag, setSelectedTag] = useState<string>('');
|
||||
const ref = useRef(null);
|
||||
const searchParams = useSearchParams();
|
||||
const t = searchParams.get('t');
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (!t) return;
|
||||
|
@ -118,13 +120,15 @@ export function VideoInteractive({ vod, safeDate, localizedDate }: IVideoInterac
|
|||
></FontAwesomeIcon>
|
||||
<span>Tags</span>
|
||||
</h4>
|
||||
<div className='tags'>
|
||||
{vod.tagVodRelations.map((tvr: ITagVodRelation) => (
|
||||
<span className="tag is-small" key={tvr.id}>{tvr.tag.name}</span>
|
||||
))}
|
||||
<Tagger vod={vod}></Tagger>
|
||||
<div>
|
||||
<div className="tags has-addons mb-5">
|
||||
{vod.tagVodRelations.map((tvr: ITagVodRelation) => (
|
||||
<Tag key={tvr.id} tvr={tvr}></Tag>
|
||||
))}
|
||||
<Tagger vod={vod}></Tagger>
|
||||
</div>
|
||||
</div>
|
||||
<Timestamps vod={vod} selectedTag={selectedTag}></Timestamps>
|
||||
<Timestamps vod={vod}></Timestamps>
|
||||
</div>
|
||||
|
||||
</VideoContext.Provider>
|
||||
|
|
|
@ -3,12 +3,13 @@ import { strapiUrl } from './constants'
|
|||
import { unmarshallTag, ITag, IToyTag } from './tags';
|
||||
import { IVod } from './vods';
|
||||
import { IAuthData } from '@/components/auth';
|
||||
import slugify from 'slugify';
|
||||
|
||||
export interface ITagVodRelation {
|
||||
id: number;
|
||||
tag: ITag | IToyTag;
|
||||
vod: IVod;
|
||||
creatorId: number;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
|
||||
|
@ -26,27 +27,12 @@ export function unmarshallTagVodRelation(d: any): ITagVodRelation {
|
|||
return {
|
||||
id: d.id,
|
||||
tag: unmarshallTag(d.attributes.tag.data),
|
||||
vod: d.attributes.vod
|
||||
vod: d.attributes.vod,
|
||||
creatorId: d.attributes.creatorId,
|
||||
createdAt: d.attributes.createdAt,
|
||||
}
|
||||
}
|
||||
|
||||
// export async function createTagVodRelationAndTag (authData: IAuthData, tagName: string, vodId: number) {
|
||||
// const res = await fetch(`${strapiUrl}/api/tag-vod-relations/tag`, {
|
||||
// method: 'POST',
|
||||
// headers: {
|
||||
// 'Authorization': `Bearer ${authData.accessToken}`,
|
||||
// 'Content-Type': 'application/json'
|
||||
// },
|
||||
// body: JSON.stringify({
|
||||
// data: {
|
||||
// tagName: slugify(tagName),
|
||||
// vodId: vodId
|
||||
// }
|
||||
// })
|
||||
// })
|
||||
// const json = await res.json();
|
||||
// return json;
|
||||
// }
|
||||
|
||||
export async function createTagAndTvr(setError: Function, authData: IAuthData, tagName: string, vodId: number) {
|
||||
if (!authData) throw new Error('Must be logged in');
|
||||
|
@ -73,31 +59,6 @@ export async function createTagAndTvr(setError: Function, authData: IAuthData, t
|
|||
}
|
||||
|
||||
|
||||
// async createTimestamp() {
|
||||
// const ts = {
|
||||
// time: parseInt(Alpine.store('player').seconds),
|
||||
// tag: this.selectedTag.tagId,
|
||||
// vod: this.vodId
|
||||
// }
|
||||
// return fetch(`${window.backend}/api/timestamps?populate=*`, {
|
||||
// method: 'POST',
|
||||
// body: JSON.stringify({
|
||||
// data: ts
|
||||
// }),
|
||||
// headers: {
|
||||
// 'Content-Type': 'application/json',
|
||||
// 'Authorization': `Bearer ${Alpine.store('auth').jwt}`
|
||||
// },
|
||||
// })
|
||||
// .then(res => res.json())
|
||||
// .then(data => {
|
||||
// if (data?.error) {
|
||||
// throw new Error(data.error.message || 'Problem while creating timestamp. Please try again later')
|
||||
// }
|
||||
// else return data.data
|
||||
// })
|
||||
// },
|
||||
|
||||
export async function getTagVodRelationsForVtuber(vtuberId: number, page: number = 1, pageSize: number = 25): Promise<ITagVodRelations> {
|
||||
// get the tag-vod-relations where the vtuber is the vtuber we are interested in.
|
||||
const query = qs.stringify(
|
||||
|
|
|
@ -31,16 +31,20 @@ export interface IRawTimestamp {
|
|||
tag: IRawTag;
|
||||
tagId: number;
|
||||
vodId: number;
|
||||
createdAt: string;
|
||||
creatorId: number;
|
||||
}
|
||||
}
|
||||
|
||||
export interface ITimestampsResponse {
|
||||
data: IRawTimestamp[];
|
||||
pagination: {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
pageCount: number;
|
||||
total: number;
|
||||
meta: {
|
||||
pagination: {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
pageCount: number;
|
||||
total: number;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -51,6 +55,24 @@ function truncateString(str: string, maxLength: number) {
|
|||
return str.substring(0, maxLength - 1) + '…';
|
||||
}
|
||||
|
||||
export function deleteTimestamp(authData: IAuthData, tsId: number) {
|
||||
return fetch(`${strapiUrl}/api/timestamps/deleteMine/${tsId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${authData.accessToken}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then((res) => {
|
||||
if (!res.ok) throw new Error(res.statusText);
|
||||
else return res.json();
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
// setError('root.serverError', { message: e.message })
|
||||
})
|
||||
}
|
||||
|
||||
export function unmarshallTimestamps(ts: any) {
|
||||
if (!ts?.attributes?.tag?.data) throw new Error(`cannot format ts id ${ts.id} because ts.attributes.tag.data is undefined`)
|
||||
const id = ts.id
|
||||
|
@ -62,7 +84,7 @@ export function unmarshallTimestamps(ts: any) {
|
|||
return { id, time, vodId, tagName, tagId, tnShort }
|
||||
}
|
||||
|
||||
export function assertTimestamp(setError: Function, authData: IAuthData, tagId: number, vodId: number, time: number) {
|
||||
export function createTimestamp(setError: Function, authData: IAuthData, tagId: number, vodId: number, time: number) {
|
||||
return fetch(`${strapiUrl}/api/timestamps/assert`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
|
|
|
@ -180,7 +180,7 @@ export async function getVodForDate(date: Date): Promise<IVod|null> {
|
|||
fields: ['cdnUrl', 'url']
|
||||
},
|
||||
tagVodRelations: {
|
||||
fields: ['tag'],
|
||||
fields: ['tag', 'createdAt', 'creatorId'],
|
||||
populate: ['tag']
|
||||
},
|
||||
videoSrcB2: {
|
||||
|
|
|
@ -7,4 +7,8 @@
|
|||
height: 2em;
|
||||
margin-bottom: 0.5rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.isTiny {
|
||||
height: 1.5em;
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
const path = require("path");
|
||||
const nextConfig = {
|
||||
output: 'standalone',
|
||||
reactStrictMode: true,
|
||||
reactStrictMode: false,
|
||||
sassOptions: {
|
||||
includePaths: [path.join(__dirname, "assets", "styles")],
|
||||
},
|
||||
|
|
|
@ -45,6 +45,7 @@
|
|||
"next-goatcounter": "^1.0.3",
|
||||
"next-react-svg": "^1.2.0",
|
||||
"nextjs-toploader": "^1.4.2",
|
||||
"p-throttle": "^5.1.0",
|
||||
"plyr": "^3.7.8",
|
||||
"plyr-react": "^5.3.0",
|
||||
"qs": "^6.11.2",
|
||||
|
|
|
@ -113,6 +113,9 @@ dependencies:
|
|||
nextjs-toploader:
|
||||
specifier: ^1.4.2
|
||||
version: 1.4.2(next@13.5.1)(react-dom@18.2.0)(react@18.2.0)
|
||||
p-throttle:
|
||||
specifier: ^5.1.0
|
||||
version: 5.1.0
|
||||
plyr:
|
||||
specifier: ^3.7.8
|
||||
version: 3.7.8
|
||||
|
@ -2722,6 +2725,11 @@ packages:
|
|||
p-timeout: 5.1.0
|
||||
dev: false
|
||||
|
||||
/p-throttle@5.1.0:
|
||||
resolution: {integrity: sha512-+N+s2g01w1Zch4D0K3OpnPDqLOKmLcQ4BvIFq3JC0K29R28vUOjWpO+OJZBNt8X9i3pFCksZJZ0YXkUGjaFE6g==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
dev: false
|
||||
|
||||
/p-timeout@5.1.0:
|
||||
resolution: {integrity: sha512-auFDyzzzGZZZdHz3BtET9VEz0SE/uMEAx7uWfGPucfzEwwe/xH0iVeZibQmANYE/hp9T2+UUZT5m+BKyrDp3Ew==}
|
||||
engines: {node: '>=12'}
|
||||
|
|
Loading…
Reference in New Issue