1
0

feat: combine the frontend and backend to work together

This commit is contained in:
Rokas Puzonas 2022-04-12 16:50:10 +00:00
parent aed9c43d2e
commit da59f31488
9 changed files with 183 additions and 46 deletions

View File

@ -1,9 +0,0 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

View File

@ -1,23 +1,70 @@
import React from 'react'; import React, { useState } from 'react';
import './App.css'; import './App.css';
import DiscRow from './components/DiscRow';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { selectDiscs, useAppSelector } from "./store"
import { solid } from '@fortawesome/fontawesome-svg-core/import.macro' import { solid } from '@fortawesome/fontawesome-svg-core/import.macro'
import { selectDiscs, useAppSelector } from './store'; import ResourcePackCustomizer from './components/ResourcePackCustomizer';
import { CreatePackPayload, createPack, Disc } from './api';
// TODO: Add waiting icon while creation is taking place
function App() { function App() {
const discs = useAppSelector(selectDiscs) const [description, setDescription] = useState("")
const store = useAppSelector(selectDiscs)
async function onClick() {
const discs: Record<string, Disc> = {}
for (var disc of store.discs) {
if (disc.title || disc.description || disc.youtubeUrl) {
const payloadDisc: Disc = {}
if (disc.title) {
payloadDisc.title = disc.title
}
if (disc.description) {
payloadDisc.description = disc.description
}
if (disc.youtubeUrl) {
payloadDisc.youtubeUrl = disc.youtubeUrl
}
discs[disc.resourceName] = payloadDisc
}
}
const info: CreatePackPayload = { discs }
if (description.length > 0) {
info.description = description
}
const blob = await createPack(info)
if (blob) {
downloadBlob(blob, "music-pack.zip")
}
}
function downloadBlob(blob: Blob, filename: string) {
const url = window.URL.createObjectURL(new Blob([blob]))
const link = document.createElement("a")
link.href = url
link.setAttribute("download", filename)
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
return ( return (
<div className="app"> <div className="app">
<header> <header>
<button> <button onClick={onClick}>
<FontAwesomeIcon className="margin-right-1" icon={solid("gears")} /> <FontAwesomeIcon className="margin-right-1" icon={solid("gears")} />
Generate resource pack Generate resource pack
</button> </button>
</header> </header>
<main> <main>
{ discs.discs.map((disc, idx) => { return <DiscRow key={idx} discId={idx} /> }) } <hr />
<ResourcePackCustomizer
description={description}
setDescription={setDescription}
/>
</main> </main>
<footer> <footer>
Made By Rokas Puzonas Made By Rokas Puzonas

41
client/src/api.ts Normal file
View File

@ -0,0 +1,41 @@
const URL = "http://localhost:3001"
export interface Disc {
title?: string
description?: string
youtubeUrl?: string
}
export interface CreatePackPayload {
description?: string
discs?: Record<string, Disc>
}
export async function getTitle(url: string): Promise<string | undefined> {
const res = await fetch(`${URL}/get-title`, {
method: "POST",
body: url,
})
if (res.status == 200) {
return await res.text()
} else {
return undefined
}
}
export async function createPack(info: CreatePackPayload) {
const res = await fetch(`${URL}/create`, {
method: "POST",
mode: "cors",
headers: { "Content-Type" : "application/json" },
body: JSON.stringify(info),
})
if (res.status == 200) {
return await res.blob()
} else {
console.log(res)
return undefined
}
}

View File

@ -4,7 +4,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import { solid } from "@fortawesome/fontawesome-svg-core/import.macro" import { solid } from "@fortawesome/fontawesome-svg-core/import.macro"
import ReactTooltip from "react-tooltip" import ReactTooltip from "react-tooltip"
import EditNameModal from "./EditNameModal" import EditNameModal from "./EditNameModal"
import { setDescription, setTitle } from "../store/discs" import { setDescription, setTitle, setYoutubeUrl } from "../store/discs"
import { useState } from "react" import { useState } from "react"
import "./DiscRow.css" import "./DiscRow.css"
@ -23,8 +23,9 @@ function DiscRow({ discId }: DiscRowProps) {
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
function onReset() { function onReset() {
dispatch(setTitle({ id: discId, title: "" })) dispatch(setTitle({ id: discId, title: undefined }))
dispatch(setDescription({ id: discId, description: "" })) dispatch(setDescription({ id: discId, description: undefined }))
dispatch(setYoutubeUrl({ id: discId, youtubeUrl: undefined }))
} }
return ( return (

View File

@ -16,17 +16,19 @@ function EditNameModal({ discId, show, onClose }: EditNameModalProps) {
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
function setInputTitle(e: React.ChangeEvent<HTMLInputElement>) { function setInputTitle(e: React.ChangeEvent<HTMLInputElement>) {
dispatch(setTitle({ if (e.target.value.length == 0) {
id: discId, dispatch(setTitle({ id: discId, title: undefined }))
title: e.target.value } else {
})) dispatch(setTitle({ id: discId, title: e.target.value }))
}
} }
function setInputDescription(e: React.ChangeEvent<HTMLInputElement>) { function setInputDescription(e: React.ChangeEvent<HTMLInputElement>) {
dispatch(setDescription({ if (e.target.value.length == 0) {
id: discId, dispatch(setDescription({ id: discId, description: undefined }))
description: e.target.value } else {
})) dispatch(setDescription({ id: discId, description: e.target.value }))
}
} }
return <Modal show={show} onClose={onClose}> return <Modal show={show} onClose={onClose}>

View File

@ -0,0 +1,23 @@
import { selectDiscs, useAppSelector } from '../store';
import DiscRow from './DiscRow';
interface ResourcePackCustomizerProps {
description: string
setDescription: { (description: string): void }
}
function ResourcePackCustomizer({ description, setDescription }: ResourcePackCustomizerProps) {
const discs = useAppSelector(selectDiscs)
return <div>
<label>Resource pack description:</label>
<input
onChange={(e) => setDescription(e.target.value)}
type="text"
value={description}
/>
{ discs.discs.map((disc, idx) => { return <DiscRow key={idx} discId={idx} /> }) }
</div>
}
export default ResourcePackCustomizer

View File

@ -1,8 +1,9 @@
import { selectDiscs, useAppDispatch, useAppSelector } from "../store" import { selectDiscs, useAppDispatch, useAppSelector } from "../store"
import { setAudioUrl } from "../store/discs" import { setDescription, setYoutubeUrl } from "../store/discs"
import { useState } from "react" import { useState } from "react"
import Modal from "./Modal" import Modal from "./Modal"
import "./UploadAudioModal.css" import "./UploadAudioModal.css"
import { getTitle } from "../api"
interface UploadAudioModalProps { interface UploadAudioModalProps {
discId: number discId: number
@ -14,24 +15,36 @@ function UploadAudioModal({ discId, show, onClose }: UploadAudioModalProps) {
const discs = useAppSelector(selectDiscs) const discs = useAppSelector(selectDiscs)
const disc = discs.discs[discId] const disc = discs.discs[discId]
const [url, setUrl] = useState("");
const [updateDescription, setUpdateDescription] = useState(true); const [updateDescription, setUpdateDescription] = useState(true);
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
async function setInputYoutubeUrl(e: React.ChangeEvent<HTMLInputElement>) {
const url = e.target.value
if (!url || url.length == 0) {
dispatch(setYoutubeUrl({ id: discId, youtubeUrl: undefined }))
} else {
dispatch(setYoutubeUrl({ id: discId, youtubeUrl: url }))
}
}
async function onModalClose() { async function onModalClose() {
onClose() onClose()
dispatch(setAudioUrl({ if (updateDescription && disc.youtubeUrl) {
id: discId, const title = await getTitle(disc.youtubeUrl)
audioUrl: url if (title) {
})) dispatch(setDescription({ id: discId, description: title }))
} else {
dispatch(setDescription({ id: discId, description: "ERROR! Video not found" }))
}
}
} }
return <Modal show={show} onClose={onModalClose}> return <Modal show={show} onClose={onModalClose}>
<label>Youtube URL:</label> <label>Youtube URL:</label>
<input <input
onChange={(e) => setUrl(e.target.value)} onChange={setInputYoutubeUrl}
type="text" type="text"
value={url} value={disc.youtubeUrl || ""}
/> />
<div className="checkbox-group"> <div className="checkbox-group">
<label>Update disc description:</label> <label>Update disc description:</label>

View File

@ -3,7 +3,8 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'
export interface Disc { export interface Disc {
title?: string title?: string
description?: string description?: string
audioUrl?: string youtubeUrl?: string
resourceName: string
defaultTitle: string defaultTitle: string
defaultDescription: string defaultDescription: string
icon: string icon: string
@ -18,58 +19,72 @@ const defaultTitle = "Music Disc"
const initialState: DiscsState = { const initialState: DiscsState = {
discs: [ discs: [
{ {
resourceName: "13",
defaultTitle, defaultDescription: "C418 - 13", defaultTitle, defaultDescription: "C418 - 13",
icon: "/sprites/music_disc_13.png" icon: "/sprites/music_disc_13.png"
}, },
{ {
resourceName: "cat",
defaultTitle, defaultDescription: "C418 - cat", defaultTitle, defaultDescription: "C418 - cat",
icon: "/sprites/music_disc_cat.png" icon: "/sprites/music_disc_cat.png"
}, },
{ {
resourceName: "blocks",
defaultTitle, defaultDescription: "C418 - blocks", defaultTitle, defaultDescription: "C418 - blocks",
icon: "/sprites/music_disc_blocks.png" icon: "/sprites/music_disc_blocks.png"
}, },
{ {
resourceName: "chirp",
defaultTitle, defaultDescription: "C418 - chirp", defaultTitle, defaultDescription: "C418 - chirp",
icon: "/sprites/music_disc_chirp.png" icon: "/sprites/music_disc_chirp.png"
}, },
{ {
resourceName: "far",
defaultTitle, defaultDescription: "C418 - far", defaultTitle, defaultDescription: "C418 - far",
icon: "/sprites/music_disc_far.png" icon: "/sprites/music_disc_far.png"
}, },
{ {
resourceName: "mall",
defaultTitle, defaultDescription: "C418 - mall", defaultTitle, defaultDescription: "C418 - mall",
icon: "/sprites/music_disc_mall.png" icon: "/sprites/music_disc_mall.png"
}, },
{ {
resourceName: "mellohi",
defaultTitle, defaultDescription: "C418 - mellohi", defaultTitle, defaultDescription: "C418 - mellohi",
icon: "/sprites/music_disc_mellohi.png" icon: "/sprites/music_disc_mellohi.png"
}, },
{ {
resourceName: "stal",
defaultTitle, defaultDescription: "C418 - stal", defaultTitle, defaultDescription: "C418 - stal",
icon: "/sprites/music_disc_stal.png" icon: "/sprites/music_disc_stal.png"
}, },
{ {
resourceName: "strad",
defaultTitle, defaultDescription: "C418 - strad", defaultTitle, defaultDescription: "C418 - strad",
icon: "/sprites/music_disc_strad.png" icon: "/sprites/music_disc_strad.png"
}, },
{ {
resourceName: "ward",
defaultTitle, defaultDescription: "C418 - ward", defaultTitle, defaultDescription: "C418 - ward",
icon: "/sprites/music_disc_ward.png" icon: "/sprites/music_disc_ward.png"
}, },
{ {
resourceName: "11",
defaultTitle, defaultDescription: "C418 - 11", defaultTitle, defaultDescription: "C418 - 11",
icon: "/sprites/music_disc_11.png" icon: "/sprites/music_disc_11.png"
}, },
{ {
resourceName: "wait",
defaultTitle, defaultDescription: "C418 - wait", defaultTitle, defaultDescription: "C418 - wait",
icon: "/sprites/music_disc_wait.png" icon: "/sprites/music_disc_wait.png"
}, },
{ {
resourceName: "otherside",
defaultTitle, defaultDescription: "Lena Raine - otherside", defaultTitle, defaultDescription: "Lena Raine - otherside",
icon: "/sprites/music_disc_otherside.png" icon: "/sprites/music_disc_otherside.png"
}, },
{ {
resourceName: "pigstep",
defaultTitle, defaultDescription: "Lena Raine - Pigstep", defaultTitle, defaultDescription: "Lena Raine - Pigstep",
icon: "/sprites/music_disc_pigstep.png" icon: "/sprites/music_disc_pigstep.png"
}, },
@ -80,21 +95,21 @@ export const discsSlice = createSlice({
name: "discs", name: "discs",
initialState, initialState,
reducers: { reducers: {
setTitle: (state, action: PayloadAction<{ id: number, title: string }>) => { setTitle: (state, action: PayloadAction<{ id: number, title: string|undefined }>) => {
const { id, title } = action.payload; const { id, title } = action.payload;
state.discs[id].title = title state.discs[id].title = title
}, },
setDescription: (state, action: PayloadAction<{ id: number, description: string }>) => { setDescription: (state, action: PayloadAction<{ id: number, description: string|undefined }>) => {
const { id, description } = action.payload; const { id, description } = action.payload;
state.discs[id].description = description state.discs[id].description = description
}, },
setAudioUrl: (state, action: PayloadAction<{ id: number, audioUrl: string }>) => { setYoutubeUrl: (state, action: PayloadAction<{ id: number, youtubeUrl: string|undefined }>) => {
const { id, audioUrl } = action.payload; const { id, youtubeUrl } = action.payload;
state.discs[id].audioUrl = audioUrl state.discs[id].youtubeUrl = youtubeUrl
} }
} }
}) })
export const { setTitle, setDescription, setAudioUrl } = discsSlice.actions export const { setTitle, setDescription, setYoutubeUrl } = discsSlice.actions
export default discsSlice.reducer export default discsSlice.reducer

View File

@ -3,7 +3,7 @@ import ytdl from "ytdl-core"
import Joi from "joi" import Joi from "joi"
import JSZip from "jszip" import JSZip from "jszip"
import ffmpeg from "fluent-ffmpeg" import ffmpeg from "fluent-ffmpeg"
import { createWriteStream, readFileSync } from "fs" import { readFileSync } from "fs"
import temp from "temp" import temp from "temp"
const availableDiscs = [ const availableDiscs = [
@ -13,7 +13,7 @@ const availableDiscs = [
interface Disc { interface Disc {
title?: string title?: string
description?: string description?: string
youtubeUrl: string youtubeUrl?: string
} }
interface CreatePackPayload { interface CreatePackPayload {
@ -73,7 +73,7 @@ function addCreateRoute(server: hapi.Server) {
const discValidator = Joi.object<Disc>({ const discValidator = Joi.object<Disc>({
title: Joi.string().optional(), title: Joi.string().optional(),
description: Joi.string().optional(), description: Joi.string().optional(),
youtubeUrl: Joi.string().required(), youtubeUrl: Joi.string().optional(),
}).optional() }).optional()
const discsValidators: Record<string, Joi.ObjectSchema<Disc>> = {} const discsValidators: Record<string, Joi.ObjectSchema<Disc>> = {}
@ -93,6 +93,7 @@ function addCreateRoute(server: hapi.Server) {
validate: { payload: payloadValidator } validate: { payload: payloadValidator }
}, },
handler: async (req, h) => { handler: async (req, h) => {
console.log("CREATE")
const payload = req.payload as CreatePackPayload const payload = req.payload as CreatePackPayload
const zip = JSZip(); const zip = JSZip();
@ -117,7 +118,7 @@ function addCreateRoute(server: hapi.Server) {
audioStreams.push(new Promise(async (resolve, reject) => { audioStreams.push(new Promise(async (resolve, reject) => {
const tempFile = temp.path() const tempFile = temp.path()
try { try {
await dowloadYoutubeAudio(url, tempFile) await dowloadYoutubeAudio(url as string, tempFile)
} catch (e) { } catch (e) {
console.error(e) console.error(e)
reject(e) reject(e)
@ -163,8 +164,11 @@ function addCreateRoute(server: hapi.Server) {
async function main() { async function main() {
const server = hapi.server({ const server = hapi.server({
port: 3000, port: process.env.PORT || 3000,
host: 'localhost', host: 'localhost',
routes: {
cors: true
}
}) })
addGetTitleRoute(server) addGetTitleRoute(server)