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 DiscRow from './components/DiscRow';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { selectDiscs, useAppSelector } from "./store"
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() {
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 (
<div className="app">
<header>
<button>
<button onClick={onClick}>
<FontAwesomeIcon className="margin-right-1" icon={solid("gears")} />
Generate resource pack
</button>
</header>
<main>
{ discs.discs.map((disc, idx) => { return <DiscRow key={idx} discId={idx} /> }) }
<hr />
<ResourcePackCustomizer
description={description}
setDescription={setDescription}
/>
</main>
<footer>
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 ReactTooltip from "react-tooltip"
import EditNameModal from "./EditNameModal"
import { setDescription, setTitle } from "../store/discs"
import { setDescription, setTitle, setYoutubeUrl } from "../store/discs"
import { useState } from "react"
import "./DiscRow.css"
@ -23,8 +23,9 @@ function DiscRow({ discId }: DiscRowProps) {
const dispatch = useAppDispatch()
function onReset() {
dispatch(setTitle({ id: discId, title: "" }))
dispatch(setDescription({ id: discId, description: "" }))
dispatch(setTitle({ id: discId, title: undefined }))
dispatch(setDescription({ id: discId, description: undefined }))
dispatch(setYoutubeUrl({ id: discId, youtubeUrl: undefined }))
}
return (

View File

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

View File

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

View File

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