'use client'; import { IVtuber } from "@/lib/vtubers"; import { IStream } from "@/lib/streams"; import { useSearchParams } from 'next/navigation'; import React, { useContext, useState, useEffect } from 'react'; import { UppyContext } from 'app/uppy'; import { LoginButton, useAuth } from '@/components/auth'; import { Dashboard } from '@uppy/react'; import styles from '@/assets/styles/fp.module.css' import { projektMelodyEpoch } from "@/lib/constants"; import add from "date-fns/add"; import sub from "date-fns/sub"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faCheckCircle, faEraser, faPaperPlane, faSpinner, faX, faXmark } from "@fortawesome/free-solid-svg-icons"; import { useForm, useFieldArray, ValidationMode } from 'react-hook-form'; import { yupResolver } from '@hookform/resolvers/yup'; import * as Yup from 'yup'; import qs from 'qs'; import { toast } from "react-toastify"; import { ErrorMessage } from "@hookform/error-message" interface IUploadFormProps { vtubers: IVtuber[]; } interface IValidationResults { valid: boolean; issues: string[] | null; } interface IFormSchema extends Yup.InferType<typeof validationSchema> { }; const validationSchema = Yup.object().shape({ vtuber: Yup.number() .required('VTuber is required'), streamCuid: Yup.string().optional(), date: Yup.date() .typeError('Invalid date') // https://stackoverflow.com/a/72985532/1004931 .min(sub(projektMelodyEpoch, { days: 1 }), 'Date must be after February 7 2020') .max(add(new Date(), { days: 1 }), 'Date cannot be in the future') .required('Date is required'), notes: Yup.string().optional(), attribution: Yup.boolean().optional(), files: Yup.array() .of( Yup.object().shape({ key: Yup.string().required('key is required'), uploadId: Yup.string().required('uploadId is required') }), ) .min(1, 'At least one file is required'), }); export default function UploadForm({ vtubers }: IUploadFormProps) { const searchParams = useSearchParams(); const cuid = searchParams.get('cuid'); const uppy = useContext(UppyContext); const { authData } = useAuth(); const formOptions = { resolver: yupResolver(validationSchema), mode: 'onChange' as keyof ValidationMode, }; const { register, handleSubmit, setError, clearErrors, formState: { errors, isValid, isSubmitted, isSubmitSuccessful, isSubmitting }, setValue, watch, reset } = useForm(formOptions); // useEffect(() => { // if (!cuid) return; // (async () => { // console.log('query') // const query = qs.stringify({ // filters: { // cuid: { // '$eq': cuid // } // } // }); // const res = await fetch(`${process.env.NEXT_PUBLIC_STRAPI_URL}/api/streams?${query}`); // if (!res.ok) return; // const matchingStream = (await res.json()).data as IStream; // console.log(matchingStream); // setValue('vtuber', matchingStream.attributes.vtuber.data.id); // })(); // }, [cuid]); // setValue('streamCuid', cuid||''); const files = watch('files'); async function createUSC(data: IFormSchema) { try { const res = await fetch(`${process.env.NEXT_PUBLIC_STRAPI_URL}/api/user-submitted-contents/createFromUppy`, { method: 'POST', headers: { 'authorization': `Bearer ${authData?.accessToken}`, 'content-type': 'application/json', 'accept': 'application/json' }, body: JSON.stringify({ data: { files: data.files, attribution: data.attribution, notes: data.notes, vtuber: data.vtuber, date: data.date, streamCuid: cuid } }) }); if (!res.ok) { console.error('failed to fetch /api/user-submitted-contents/createFromUppy'); const body = await res.json(); const error = body.error; toast.error(`${error.type} ${error.message}`, { theme: 'dark' }); setError('root.serverError', { type: error.type, message: error.message }) } } catch (e) { if (e instanceof Error) { toast.error(`${e.message}`, { theme: 'dark' }); setError('root.serverError', { type: "remote", message: e.message, }); } else { toast.error(`Something went wrong. Please try again.`, { theme: 'dark' }); setError('root.serverError', { type: 'remote', message: 'Something went wrong. Please try again.' }) } } } uppy.on('complete', async (result: any) => { let files = result.successful.map((f: any) => ({ key: f.s3Multipart.key, uploadId: f.s3Multipart.uploadId })); setValue('files', files); }); return (<div className="notification is-secondary"> <h1 className="title">VOD uploads</h1> <p> <i>coming soon!!</i> </p> <hr ></hr> <p>Track progress on the <a href="/goals">Goals Page</a></p> </div>) // return ( // <> // <div className='section'> // <h2 className='title is-2'>Upload VOD</h2> // <p className="mb-5"><i>Together we can archive all lewdtuber livestreams!</i></p> // {(!authData?.accessToken) // ? // <> // <aside className='notification is-danger'><p>Please log in to upload VODs</p></aside> // <LoginButton /> // </> // : ( // <div className='columns is-multiline'> // <form id="vod-details" onSubmit={handleSubmit(createUSC)}> // {(!isSubmitSuccessful) && <div className='column is-full'> // <section className="hero is-info mb-3"> // <div className="hero-body"> // <p className="title"> // Step 1 // </p> // <p className="subtitle"> // Upload the file // </p> // </div> // </section> // <section className="section mb-5"> // <Dashboard // uppy={uppy} // theme='dark' // proudlyDisplayPoweredByUppy={false} // /> // <input // required // hidden={true} // style={{ display: 'none' }} // className="input" type="text" // {...register('files')} // ></input> // {errors.files && <p className="help is-danger">{errors.files.message?.toString()}</p>} // </section> // </div>} // {(!isSubmitSuccessful) && <div className='column is-full '> // {/* {(!cuid) && <aside className='notification is-info'>Hint: Some of these fields are filled out automatically when uploading from a <Link href="/streams">stream</Link> page.</aside>} */} // <section className="hero is-info mb-3"> // <div className="hero-body"> // <p className="title"> // Step 2 // </p> // <p className="subtitle"> // Tell us about the VOD // </p> // </div> // </section> // <section className="section"> // {/* <input // required // // hidden={false} // // style={{ display: 'none' }} // className="input" type="text" // {...register('streamCuid')} // ></input> */} // <div className="field"> // <label className="label">VTuber</label> // <div className="select"> // <select // required // // value={vtuber} // // onChange={(evt) => setVtuber(parseInt(evt.target.value))} // {...register('vtuber')} // > // {vtubers.map((vtuber: IVtuber) => ( // <option key={vtuber.id} value={vtuber.id}>{vtuber.attributes.displayName}</option> // ))} // </select> // </div> // <p className="help is-info">Choose the VTuber this VOD belongs to. (More VTubers will be added when storage/bandwidth funding is secured.)</p> // {errors.vtuber && <p className="help is-danger">vtuber error</p>} // </div> // <div className="field"> // <label className="label">Stream Date</label> // <input // required // className="input" type="date" // {...register('date')} // // onChange={(evt) => setDate(evt.target.value)} // ></input> // <p className="help is-info">The date when the VOD was originally streamed.</p> // {errors.date && <p className="help is-danger">{errors.date.message?.toString()}</p>} // </div> // <div className="field"> // <label className="label">Notes</label> // <textarea // className="textarea" // placeholder="e.g. Missing first 10 minutes of stream" // // onChange={(evt) => setNote(evt.target.value)} // {...register('notes')} // ></textarea> // <p className="help is-info">If there are any issues with the VOD, put a note here. If there are no VOD issues, leave this field blank.</p> // </div> // <div className="field"> // <label className="label">Attribution</label> // <label className="checkbox"> // <input // type="checkbox" // // onChange={(evt) => setAttribution(evt.target.checked)} // {...register('attribution')} // /> // <span className={`ml-2 ${styles.noselect}`}>Credit {authData.user?.username} for the upload.</span> // <p className="help is-info">Check this box if you want your username displayed on the website. Thank you for uploading!</p> // </label> // </div> // </section> // </div>} // <div className="column is-full"> // <section className="hero is-info"> // <div className="hero-body"> // <p className="title"> // Step 3 // </p> // <p className="subtitle"> // Send the form // </p> // </div> // </section> // <section className="section"> // {errors.root?.serverError && ( // <div className="notification"> // <button className="delete" onClick={() => clearErrors()}></button> // <ErrorMessage name="root" errors={errors} ></ErrorMessage> // </div> // )} // {!isSubmitSuccessful && ( // <button className="button is-primary is-large mt-5"> // <span className="icon is-small"> // <FontAwesomeIcon icon={faPaperPlane}></FontAwesomeIcon> // </span> // <span>Send</span> // </button> // )} // {isSubmitting && ( // <p> // <FontAwesomeIcon className="mt-5 fa-spin-pulse" icon={faSpinner} ></FontAwesomeIcon> // </p> // )} // {isSubmitSuccessful && ( // <> // <aside className="notification mt-5 is-success">Thank you for uploading! </aside> // <button onClick={() => { // reset(); // reset form // const files = uppy.getFiles() // for (const file of files) { // uppy.removeFile(file.id); // reset uppy // } // }} className="button is-primary"> // <span className="icon is-small"> // <FontAwesomeIcon icon={faEraser}></FontAwesomeIcon> // </span> // <span>Reset form</span> // </button> // </> // )} // </section> // </div> // </form> // </div> // ) // } // </div> // </> // ) }