2024-01-20 16:16:14 +00:00
'use client' ;
2024-07-10 22:11:18 +00:00
2024-07-15 16:07:04 +00:00
import { IVtuber } from "@futureporn/types" ;
2024-01-20 16:16:14 +00:00
import { useSearchParams } from 'next/navigation' ;
2024-07-10 22:11:18 +00:00
import React from 'react' ;
2024-07-10 02:34:23 +00:00
import AwsS3 from '@uppy/aws-s3' ;
import RemoteSources from '@uppy/remote-sources' ;
2024-07-10 22:11:18 +00:00
import { LoginButton , useAuth } from '@/app/components/auth' ;
2024-01-20 16:16:14 +00:00
import { Dashboard } from '@uppy/react' ;
import styles from '@/assets/styles/fp.module.css'
2024-07-10 22:11:18 +00:00
import { projektMelodyEpoch } from "@/app/lib/constants" ;
2024-01-20 16:16:14 +00:00
import add from "date-fns/add" ;
import sub from "date-fns/sub" ;
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" ;
2024-07-10 22:11:18 +00:00
import { faEraser , faPaperPlane , faSpinner , faX , faXmark } from "@fortawesome/free-solid-svg-icons" ;
import { useForm , ValidationMode } from 'react-hook-form' ;
2024-01-20 16:16:14 +00:00
import { yupResolver } from '@hookform/resolvers/yup' ;
import * as Yup from 'yup' ;
2024-02-02 23:50:24 +00:00
import { toast } from "react-toastify" ;
import { ErrorMessage } from "@hookform/error-message"
2024-07-10 02:34:23 +00:00
import Uppy from '@uppy/core' ;
2024-07-10 22:11:18 +00:00
import { companionUrl } from '@/app/lib/constants' ;
2024-07-10 02:34:23 +00:00
2024-01-20 16:16:14 +00:00
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' ) ,
2024-02-02 23:50:24 +00:00
streamCuid : Yup.string ( ) . optional ( ) ,
2024-01-20 16:16:14 +00:00
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' ) ;
2024-07-10 02:34:23 +00:00
2024-01-20 16:16:14 +00:00
const { authData } = useAuth ( ) ;
2024-07-10 02:34:23 +00:00
const uppy = new Uppy (
{
autoProceed : true ,
debug : true ,
logger : {
debug : console.info ,
warn : console.log ,
error : console.error
} ,
}
)
. use ( RemoteSources , {
companionUrl ,
sources : [
'GoogleDrive' ,
'Dropbox' ,
'Url'
]
} )
. use ( AwsS3 , {
companionUrl ,
shouldUseMultipart : true ,
abortMultipartUpload : ( ) = > { } , // @see https://github.com/transloadit/uppy/issues/1197#issuecomment-491756118
companionHeaders : {
'authorization' : ` Bearer ${ authData ? . accessToken } `
}
} )
2024-02-02 23:50:24 +00:00
2024-01-20 16:16:14 +00:00
const formOptions = {
resolver : yupResolver ( validationSchema ) ,
mode : 'onChange' as keyof ValidationMode ,
} ;
const {
register ,
handleSubmit ,
2024-02-02 23:50:24 +00:00
setError ,
clearErrors ,
2024-01-20 16:16:14 +00:00
formState : {
errors ,
2024-02-02 23:50:24 +00:00
isValid ,
isSubmitted ,
isSubmitSuccessful ,
isSubmitting
2024-01-20 16:16:14 +00:00
} ,
setValue ,
watch ,
2024-02-02 23:50:24 +00:00
reset
2024-01-20 16:16:14 +00:00
} = useForm ( formOptions ) ;
2024-02-02 23:50:24 +00:00
// 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||'');
2024-01-20 16:16:14 +00:00
const files = watch ( 'files' ) ;
async function createUSC ( data : IFormSchema ) {
2024-02-02 23:50:24 +00:00
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.'
} )
}
2024-01-20 16:16:14 +00:00
}
}
uppy . on ( 'complete' , async ( result : any ) = > {
2024-07-06 08:49:51 +00:00
for ( const s of result . successful ) {
if ( ! s ? . s3Multipart ) {
2024-07-10 02:34:23 +00:00
const m = 'file was missing s3Multipart'
toast . error ( ` ${ m } ` , { theme : 'dark' } ) ;
2024-07-06 08:49:51 +00:00
setError ( 'root.serverError' , {
type : 'remote' ,
2024-07-10 02:34:23 +00:00
message : m
2024-07-06 08:49:51 +00:00
} )
2024-07-10 02:34:23 +00:00
throw new Error ( m )
2024-07-06 08:49:51 +00:00
}
}
2024-07-10 02:34:23 +00:00
console . log ( 'uppy complete! ' )
console . log ( result )
toast . success ( ` upload complete ` ) ;
2024-01-20 16:16:14 +00:00
let files = result . successful . map ( ( f : any ) = > ( { key : f.s3Multipart.key , uploadId : f.s3Multipart.uploadId } ) ) ;
setValue ( 'files' , files ) ;
} ) ;
2024-03-29 07:28:02 +00:00
2024-07-06 08:49:51 +00:00
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 = { true }
showProgressDetails = { true }
/ >
2024-07-10 02:34:23 +00:00
{ / *
Here is how we upload the files to the server .
From uppy , we get a list of files .
we add the files to a hidden input box .
the input box is part of the form which gets POSTed .
* / }
2024-07-06 08:49:51 +00:00
< input
required
2024-07-10 02:34:23 +00:00
hidden = { true }
2024-07-06 08:49:51 +00:00
style = { { display : 'block' } }
className = "input" type = "text"
{ . . . register ( 'files' ) }
> < / input >
2024-03-29 07:28:02 +00:00
2024-07-10 02:34:23 +00:00
< button className = "button" onClick = { ( ) = > { setValue ( 'files' , [
{
"key" : "4b4063a2-6b57-48f1-8565-a12ddce473e9-E1tB0KoUcAYJTni.jpg" ,
"uploadId" : "4_z7d53875ff1c32a1983d30b18_f2129582707239923_d20240708_m003328_c000_v0001086_t0006_u01720398808368"
}
] ) ; } } > ( Debug ) Add a list of files < / button >
2024-03-29 07:28:02 +00:00
2024-07-06 08:49:51 +00:00
{ 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" >
{ / * < i n p u t
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 && (
< >
2024-07-10 02:34:23 +00:00
< aside className = "notification mt-5 is-success" > Thank you for uploading ! A moderator will review the VOD before being published . < / aside >
2024-07-06 08:49:51 +00:00
< 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 >
< / >
)
2024-01-20 16:16:14 +00:00
}