241 lines
8.8 KiB
TypeScript
241 lines
8.8 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useCallback, useEffect, useContext } from 'react';
|
|
import { IVod } from '@/app/lib/vods';
|
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
import { faPlus, faX, faTags } from "@fortawesome/free-solid-svg-icons";
|
|
import { formatTimestamp } from '@/app/lib/dates';
|
|
import { readOrCreateTagVodRelation } from '@/app/lib/tag-vod-relations';
|
|
import { readOrCreateTag } from '@/app/lib/tags';
|
|
import { useAuth } from './auth';
|
|
import { debounce } from 'lodash';
|
|
import { strapiUrl } from '@/app/lib/constants';
|
|
import { VideoContext } from './video-context';
|
|
import { useForm } from "react-hook-form";
|
|
import { ITimestamp, createTimestamp } from '@/app/lib/timestamps';
|
|
import { useRouter } from 'next/navigation';
|
|
import styles from '@/assets/styles/fp.module.css'
|
|
import qs from 'qs';
|
|
import { toast } from 'react-toastify';
|
|
import slugify from 'slugify';
|
|
|
|
interface ITaggerProps {
|
|
vod: IVod;
|
|
setTimestamps: Function;
|
|
}
|
|
|
|
export interface ITagSuggestion {
|
|
id: number;
|
|
name: string;
|
|
createdAt: string;
|
|
}
|
|
|
|
|
|
type FormData = {
|
|
tagName: string;
|
|
isTimestamp: boolean;
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export function Tagger({ vod, setTimestamps }: ITaggerProps): React.JSX.Element {
|
|
|
|
const { register, setValue, setError, setFocus, handleSubmit, watch, clearErrors, formState: { errors } } = useForm<FormData>({
|
|
defaultValues: {
|
|
tagName: '',
|
|
isTimestamp: true
|
|
}
|
|
});
|
|
const [isEditor, setIsEditor] = useState<boolean>(false);
|
|
const [isAuthed, setIsAuthed] = useState<boolean>(false);
|
|
const [tagSuggestions, setTagSuggestions] = useState<ITagSuggestion[]>([]);
|
|
const { authData } = useAuth();
|
|
const { timeStamp, tvrs, setTvrs } = useContext(VideoContext);
|
|
const router = useRouter();
|
|
|
|
const request = debounce((value: string) => {
|
|
search(value);
|
|
}, 300);
|
|
|
|
const debounceRequest = useCallback((v: string) => request(v), [request]);
|
|
|
|
|
|
// Callback version of watch. It's your responsibility to unsubscribe when done.
|
|
useEffect(() => {
|
|
const subscription = watch((value, { name, type }) => {
|
|
const tagNameValue = value.tagName as string;
|
|
if (name === 'tagName' && type === 'change' && value.tagName !== '') debounceRequest(tagNameValue);
|
|
});
|
|
return () => subscription.unsubscribe();
|
|
}, [watch, debounceRequest]);
|
|
|
|
|
|
useEffect(() => {
|
|
if (isEditor) {
|
|
setFocus('tagName');
|
|
getRandomSuggestions();
|
|
}
|
|
}, [isEditor, setFocus]);
|
|
|
|
useEffect(() => {
|
|
if (authData?.accessToken) {
|
|
setIsAuthed(true);
|
|
}
|
|
}, [isAuthed, authData]);
|
|
|
|
|
|
async function getRandomSuggestions() {
|
|
const res = await fetch(`${strapiUrl}/api/tag/random`);
|
|
const tags = await res.json();
|
|
setTagSuggestions(tags)
|
|
}
|
|
|
|
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}`, {
|
|
headers: {
|
|
'Authorization': `Bearer ${authData?.accessToken}`
|
|
}
|
|
})
|
|
const json = await res.json()
|
|
if (!res.ok) {
|
|
toast('failed to get recomended tags', { type: 'error', theme: 'dark' });
|
|
} else {
|
|
setTagSuggestions(json.tags)
|
|
}
|
|
}
|
|
|
|
|
|
async function onError(errors: any) {
|
|
console.error('submit handler encoutnered an error');
|
|
console.error(errors);
|
|
toast('there was an error');
|
|
}
|
|
|
|
async function onSubmit(values: { tagName: string, isTimestamp: boolean }) {
|
|
if (!authData?.accessToken) {
|
|
toast('must be logged in', { type: 'error', theme: 'dark' });
|
|
return
|
|
}
|
|
try {
|
|
|
|
const tag = await readOrCreateTag(authData.accessToken, slugify(values.tagName));
|
|
if (!tag) throw new Error(`readOrCreateTag failed`);
|
|
|
|
|
|
const tvr = await readOrCreateTagVodRelation(authData.accessToken, tag.id, vod.id);
|
|
console.log(`now we check to see if we have a TVR`);
|
|
console.log(tvr)
|
|
|
|
if (values.isTimestamp) {
|
|
console.log(`user specified that we must create a timestamp`);
|
|
const timestamp = await createTimestamp(authData, tag.id, vod.id, timeStamp);
|
|
console.log(timestamp)
|
|
if (!timestamp) throw new Error(`failed to create timestamp`)
|
|
setTimestamps((prevTimestamps: ITimestamp[]) => [...prevTimestamps, timestamp]);
|
|
}
|
|
|
|
setValue('tagName', '');
|
|
router.refresh();
|
|
} catch (e) {
|
|
toast(`${e}`, { type: 'error', theme: 'dark' });
|
|
}
|
|
}
|
|
|
|
// if (!isAuthed) {
|
|
// return <></>
|
|
// } else {
|
|
if (isEditor) {
|
|
return (
|
|
<div className='card mt-2' style={{ width: '100%' }}>
|
|
|
|
|
|
<header className='card-header'>
|
|
<p className='card-header-title'><FontAwesomeIcon className='mr-2' icon={faTags}></FontAwesomeIcon>Tagger</p>
|
|
<button onClick={() => {
|
|
setIsEditor(false);
|
|
setValue('tagName', '');
|
|
setTagSuggestions([]);
|
|
clearErrors();
|
|
}} className='card-header-icon'>
|
|
<span className='icon'>
|
|
<FontAwesomeIcon
|
|
icon={faX}
|
|
className="fas fa-x"
|
|
></FontAwesomeIcon>
|
|
</span>
|
|
</button>
|
|
</header>
|
|
<div className='card-content'>
|
|
<form onSubmit={handleSubmit(onSubmit, onError)}>
|
|
<div className='mb-2'>
|
|
<label htmlFor="name" className='heading'>Add a tag</label>
|
|
<input
|
|
required
|
|
className="input"
|
|
placeholder="cum"
|
|
autoComplete='off'
|
|
maxLength={256}
|
|
minLength={3}
|
|
type="text"
|
|
{...register('tagName')}
|
|
></input>
|
|
</div>
|
|
<div className='mb-2'>
|
|
<span className='heading'>Suggestions</span>
|
|
{tagSuggestions.length > 0 && tagSuggestions.map((tag: ITagSuggestion) => (<button type="button" key={tag.id} className='button is-small is-rounded mr-1 is-success mb-1' onClick={() => setValue('tagName', tag.name)}>{tag.name}</button>))}
|
|
</div>
|
|
<div className={`mb-2 bulma-unselectable-mixin`}>
|
|
<label className={`checkbox ${styles.noselect}`}>
|
|
<input
|
|
className="mr-1"
|
|
type="checkbox"
|
|
{...register('isTimestamp')}
|
|
></input>
|
|
Timestamp {formatTimestamp(timeStamp)}
|
|
</label>
|
|
</div>
|
|
|
|
<div>
|
|
{(!!errors?.root?.serverError) && <div className="notification is-danger">{errors.root.serverError.message}</div>}
|
|
<input
|
|
className='button is-primary'
|
|
type="submit"
|
|
value={`Create Tag${(watch('isTimestamp')) ? ' & Timestamp': ''}`}
|
|
>
|
|
</input>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
)
|
|
} else {
|
|
return (
|
|
<button className={`button is-small is-success ${styles.tagButton}`} onClick={() => setIsEditor(true)}>
|
|
<FontAwesomeIcon
|
|
icon={faPlus}
|
|
className="fab fa-plus mr-1"
|
|
></FontAwesomeIcon>
|
|
<span>Add a Tag</span>
|
|
</button>
|
|
);
|
|
}
|
|
// }
|
|
|
|
|
|
}
|