fp/services/next/app/components/tagger.tsx

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>
);
}
// }
}