11"use client" ;
2- import { useEffect , useState , useCallback } from "react" ;
2+ import { useEffect , useState , useCallback , useRef } from "react" ;
33import { copyText } from "@/lib/clipboard" ;
44
55type Org = { id : string ; name : string } ;
@@ -29,7 +29,7 @@ export default function Dashboard() {
2929 const [ teamOrg , setTeamOrg ] = useState ( "" ) ;
3030 const [ projName , setProjName ] = useState ( "" ) ;
3131 const [ projTeam , setProjTeam ] = useState ( "" ) ;
32- const [ tab , setTab ] = useState < "page" | "waitlist" | "auctions" | "webhooks" | "affiliates" > ( "page" ) ;
32+ const [ tab , setTab ] = useState < "page" | "videos" | " waitlist" | "auctions" | "webhooks" | "affiliates" > ( "page" ) ;
3333
3434 const say = ( t : string , ok = true ) => setMsg ( { t, ok } ) ;
3535
@@ -84,6 +84,7 @@ export default function Dashboard() {
8484
8585 < div className = "tabs" >
8686 < button className = { `tab${ tab === "page" ? " on" : "" } ` } onClick = { ( ) => setTab ( "page" ) } > Domains</ button >
87+ < button className = { `tab${ tab === "videos" ? " on" : "" } ` } onClick = { ( ) => setTab ( "videos" ) } > Videos</ button >
8788 < button className = { `tab${ tab === "waitlist" ? " on" : "" } ` } onClick = { ( ) => setTab ( "waitlist" ) } > Waitlist</ button >
8889 < button className = { `tab${ tab === "auctions" ? " on" : "" } ` } onClick = { ( ) => setTab ( "auctions" ) } > Auctions</ button >
8990 < button className = { `tab${ tab === "webhooks" ? " on" : "" } ` } onClick = { ( ) => setTab ( "webhooks" ) } > Webhooks</ button >
@@ -96,6 +97,8 @@ export default function Dashboard() {
9697 < DomainWebhooksPanel onError = { ( m ) => say ( m , false ) } onOk = { ( m ) => say ( m , true ) } />
9798 ) : tab === "auctions" ? (
9899 < AuctionsPanel onError = { ( m ) => say ( m , false ) } onOk = { ( m ) => say ( m , true ) } />
100+ ) : tab === "videos" ? (
101+ < VideosPanel onError = { ( m ) => say ( m , false ) } onOk = { ( m ) => say ( m , true ) } />
99102 ) : tab === "waitlist" ? (
100103 < WaitlistPanel onError = { ( m ) => say ( m , false ) } />
101104 ) : (
@@ -462,6 +465,115 @@ function AccountPanel({ onError, onOk }: { onError: (m: string) => void; onOk: (
462465 ) ;
463466}
464467
468+ function VideosPanel ( { onError, onOk } : { onError : ( m : string ) => void ; onOk : ( m : string ) => void } ) {
469+ const [ domains , setDomains ] = useState < any [ ] | undefined > ( undefined ) ;
470+ const [ active , setActive ] = useState < string | null > ( null ) ;
471+ const [ reels , setReels ] = useState < any [ ] | null > ( null ) ;
472+ const [ title , setTitle ] = useState ( "" ) ;
473+ const [ file , setFile ] = useState < File | null > ( null ) ;
474+ const [ busy , setBusy ] = useState ( false ) ;
475+ const inputRef = useRef < HTMLInputElement > ( null ) ;
476+
477+ const load = async ( dn : string ) => {
478+ setActive ( dn ) ; setReels ( null ) ;
479+ try {
480+ const r = await fetch ( `/api/media?dn=${ encodeURIComponent ( dn ) } ` ) ;
481+ const d = await r . json ( ) ;
482+ if ( ! r . ok ) throw new Error ( d . error ) ;
483+ setReels ( d . media || [ ] ) ;
484+ } catch ( e : any ) { onError ( e . message || "Failed to load." ) ; setReels ( [ ] ) ; }
485+ } ;
486+
487+ useEffect ( ( ) => {
488+ fetch ( "/api/account" ) . then ( ( r ) => r . json ( ) ) . then ( ( d ) => {
489+ const list = [ ...( d . parkedDomains || [ ] ) ] ;
490+ // Admins can also populate the public moshcoding.com /videos gallery.
491+ if ( d . account ?. is_admin && ! list . some ( ( x : any ) => x . domain === "moshcoding.com" ) ) {
492+ list . unshift ( { domain : "moshcoding.com" } ) ;
493+ }
494+ setDomains ( list ) ;
495+ if ( list [ 0 ] ) load ( list [ 0 ] . domain ) ;
496+ } ) . catch ( ( ) => setDomains ( [ ] ) ) ;
497+ // eslint-disable-next-line react-hooks/exhaustive-deps
498+ } , [ ] ) ;
499+
500+ const upload = async ( ) => {
501+ if ( ! active || ! file ) return ;
502+ setBusy ( true ) ;
503+ try {
504+ const fd = new FormData ( ) ;
505+ fd . append ( "dn" , active ) ;
506+ fd . append ( "file" , file ) ;
507+ if ( title . trim ( ) ) fd . append ( "title" , title . trim ( ) ) ;
508+ const r = await fetch ( "/api/media" , { method : "POST" , body : fd } ) ;
509+ const d = await r . json ( ) ;
510+ if ( ! r . ok ) throw new Error ( d . error ) ;
511+ onOk ( "Reel uploaded. 🤘" ) ;
512+ setTitle ( "" ) ; setFile ( null ) ;
513+ if ( inputRef . current ) inputRef . current . value = "" ;
514+ await load ( active ) ;
515+ } catch ( e : any ) { onError ( e . message || "Upload failed." ) ; } finally { setBusy ( false ) ; }
516+ } ;
517+
518+ const del = async ( id : string ) => {
519+ if ( typeof window !== "undefined" && ! window . confirm ( "Delete this reel?" ) ) return ;
520+ setBusy ( true ) ;
521+ try {
522+ const r = await fetch ( `/api/media/${ id } ` , { method : "DELETE" } ) ;
523+ const d = await r . json ( ) . catch ( ( ) => ( { } ) ) ;
524+ if ( ! r . ok ) throw new Error ( d . error || "Delete failed." ) ;
525+ onOk ( "Deleted." ) ;
526+ if ( active ) await load ( active ) ;
527+ } catch ( e : any ) { onError ( e . message || "Delete failed." ) ; } finally { setBusy ( false ) ; }
528+ } ;
529+
530+ if ( domains === undefined ) return < section className = "card2" > < p className = "sub" > Loading…</ p > </ section > ;
531+ if ( ! domains . length ) {
532+ return < section className = "card2" > < h2 > Videos</ h2 > < p className = "sub" > No parked domains yet — claim one on the “Domains” tab and you can upload reels for it here.</ p > </ section > ;
533+ }
534+
535+ return (
536+ < section className = "card2" >
537+ < h2 > Videos</ h2 >
538+ < p className = "sub" > Upload mp4 reels per parked domain. Reels for moshcoding.com also show on the public < a href = "/videos" > /videos</ a > gallery.</ p >
539+ < div className = "tabs" style = { { flexWrap : "wrap" } } >
540+ { domains . map ( ( d ) => (
541+ < button key = { d . domain } className = { `tab${ active === d . domain ? " on" : "" } ` } onClick = { ( ) => load ( d . domain ) } > { d . domain } </ button >
542+ ) ) }
543+ </ div >
544+ { active && (
545+ < >
546+ < h3 className = "ed-h" > Upload a reel < span className = "muted" > (mp4 / webm / mov, max 100 MB)</ span > </ h3 >
547+ < div className = "row" > < input className = "inp" placeholder = "Title (optional)" value = { title } onChange = { ( e ) => setTitle ( e . target . value ) } /> </ div >
548+ < div className = "row" >
549+ < input ref = { inputRef } className = "inp" type = "file" accept = "video/mp4,video/webm,video/quicktime" onChange = { ( e ) => setFile ( e . target . files ?. [ 0 ] || null ) } />
550+ < button className = "btn2" disabled = { busy || ! file } onClick = { upload } > { busy ? "Uploading…" : "Upload" } </ button >
551+ </ div >
552+
553+ < h3 className = "ed-h" style = { { marginTop : 18 } } > Reels ({ reels ? reels . length : "…" } )</ h3 >
554+ { reels === null ? (
555+ < p className = "sub" > Loading…</ p >
556+ ) : reels . length === 0 ? (
557+ < p className = "sub" > No reels yet — upload one above.</ p >
558+ ) : (
559+ < div className = "video-grid" >
560+ { reels . map ( ( m ) => (
561+ < figure key = { m . id } className = "video-cell" >
562+ < video src = { m . url } controls preload = "metadata" playsInline />
563+ < figcaption className = "row" style = { { justifyContent : "space-between" , alignItems : "center" } } >
564+ < span > { m . title } </ span >
565+ < button className = "btn2 ghost" disabled = { busy } onClick = { ( ) => del ( m . id ) } > Delete</ button >
566+ </ figcaption >
567+ </ figure >
568+ ) ) }
569+ </ div >
570+ ) }
571+ </ >
572+ ) }
573+ </ section >
574+ ) ;
575+ }
576+
465577function WaitlistPanel ( { onError } : { onError : ( m : string ) => void } ) {
466578 const [ domains , setDomains ] = useState < any [ ] | undefined > ( undefined ) ;
467579 const [ active , setActive ] = useState < string | null > ( null ) ;
0 commit comments