Now only when the user submits the listening exam are the mp3 are uploaded onto firebase bucket
This commit is contained in:
40
src/pages/api/exam/media/[...module].ts
Normal file
40
src/pages/api/exam/media/[...module].ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { withIronSessionApiRoute } from "iron-session/next";
|
||||
import { sessionOptions } from "@/lib/session";
|
||||
import axios from "axios";
|
||||
import queryToURLSearchParams from "@/utils/query.to.url.params";
|
||||
|
||||
export default withIronSessionApiRoute(handler, sessionOptions);
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method === "POST") return post(req, res);
|
||||
|
||||
return res.status(404).json({ ok: false });
|
||||
}
|
||||
|
||||
async function post(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (!req.session.user) return res.status(401).json({ ok: false });
|
||||
|
||||
|
||||
const queryParams = queryToURLSearchParams(req);
|
||||
let endpoint = queryParams.getAll('module').join("/");
|
||||
|
||||
const response = await axios.post(
|
||||
`${process.env.BACKEND_URL}/${endpoint}/media`,
|
||||
req.body,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${process.env.BACKEND_JWT}`,
|
||||
'Accept': 'audio/mpeg'
|
||||
},
|
||||
responseType: 'arraybuffer',
|
||||
}
|
||||
);
|
||||
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'audio/mpeg',
|
||||
'Content-Length': response.data.length
|
||||
});
|
||||
|
||||
res.end(response.data);
|
||||
}
|
||||
68
src/pages/api/storage/index.ts
Normal file
68
src/pages/api/storage/index.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { ref, uploadBytes, getDownloadURL } from 'firebase/storage';
|
||||
import { IncomingForm, Files, Fields } from 'formidable';
|
||||
import { promises as fs } from 'fs';
|
||||
import { withIronSessionApiRoute } from 'iron-session/next';
|
||||
import { storage } from '@/firebase';
|
||||
import { sessionOptions } from '@/lib/session';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
export const config = {
|
||||
api: {
|
||||
bodyParser: false,
|
||||
},
|
||||
};
|
||||
|
||||
export default withIronSessionApiRoute(handler, sessionOptions);
|
||||
|
||||
export async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (!req.session.user) {
|
||||
res.status(401).json({ error: 'Not authorized' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.method !== 'POST') {
|
||||
return res.status(405).json({ error: 'Not allowed' });
|
||||
}
|
||||
|
||||
const directory = (req.query.directory as string) || "uploads";
|
||||
|
||||
try {
|
||||
const form = new IncomingForm({
|
||||
keepExtensions: true,
|
||||
multiples: true,
|
||||
});
|
||||
|
||||
const [fields, files]: [Fields, Files] = await new Promise((resolve, reject) => {
|
||||
form.parse(req, (err, fields, files) => {
|
||||
if (err) reject(err);
|
||||
resolve([fields, files]);
|
||||
});
|
||||
});
|
||||
|
||||
const fileArray = files.file;
|
||||
if (!fileArray) {
|
||||
return res.status(400).json({ error: 'No files provided' });
|
||||
}
|
||||
|
||||
const filesToProcess = Array.isArray(fileArray) ? fileArray : [fileArray];
|
||||
|
||||
const uploadPromises = filesToProcess.map(async (file) => {
|
||||
const split = file.originalFilename?.split('.') || ["bin"];
|
||||
const extension = split[split.length - 1];
|
||||
|
||||
const buffer = await fs.readFile(file.filepath);
|
||||
const storageRef = ref(storage, `${directory}/${v4()}.${extension}`);
|
||||
await uploadBytes(storageRef, buffer);
|
||||
const downloadURL = await getDownloadURL(storageRef);
|
||||
await fs.unlink(file.filepath);
|
||||
return downloadURL;
|
||||
});
|
||||
|
||||
const urls = await Promise.all(uploadPromises);
|
||||
res.status(200).json({ urls });
|
||||
} catch (error) {
|
||||
console.error('Upload error:', error);
|
||||
res.status(500).json({ error: 'Upload failed' });
|
||||
}
|
||||
}
|
||||
@@ -18,8 +18,11 @@ import ExamEditor from "@/components/ExamEditor";
|
||||
import MultipleAudioUploader from "@/components/ExamEditor/Shared/AudioEdit";
|
||||
import { redirect, serialize } from "@/utils";
|
||||
import { requestUser } from "@/utils/api";
|
||||
import { useEffect } from "react";
|
||||
import { Exercise, InteractiveSpeakingExercise, ListeningPart, SpeakingExercise } from "@/interfaces/exam";
|
||||
import { type } from "os";
|
||||
|
||||
export const getServerSideProps = withIronSessionSsr(async ({req, res}) => {
|
||||
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
||||
const user = await requestUser(req, res)
|
||||
if (!user) return redirect("/login")
|
||||
|
||||
@@ -27,16 +30,56 @@ export const getServerSideProps = withIronSessionSsr(async ({req, res}) => {
|
||||
return redirect("/")
|
||||
|
||||
return {
|
||||
props: serialize({user}),
|
||||
props: serialize({ user }),
|
||||
};
|
||||
}, sessionOptions);
|
||||
|
||||
export default function Generation({ user }: { user: User; }) {
|
||||
const { title, currentModule, dispatch } = useExamEditorStore();
|
||||
const { title, currentModule, modules, dispatch } = useExamEditorStore();
|
||||
|
||||
const updateRoot = (updates: Partial<ExamEditorStore>) => {
|
||||
dispatch({ type: 'UPDATE_ROOT', payload: { updates } });
|
||||
};
|
||||
dispatch({ type: 'UPDATE_ROOT', payload: { updates } });
|
||||
};
|
||||
|
||||
// media cleanup on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
const state = modules;
|
||||
state.listening.sections.forEach(section => {
|
||||
const listeningPart = section.state as ListeningPart;
|
||||
if (listeningPart.audio?.source) {
|
||||
URL.revokeObjectURL(listeningPart.audio.source);
|
||||
dispatch({type: "UPDATE_SECTION_SINGLE_FIELD", payload: {
|
||||
sectionId: section.sectionId, module: "listening", field: "state", value: {...listeningPart, audio: undefined}
|
||||
}})
|
||||
}
|
||||
});
|
||||
|
||||
state.speaking.sections.forEach(section => {
|
||||
const sectionState = section.state as Exercise;
|
||||
if (sectionState.type === 'speaking') {
|
||||
const speakingExercise = sectionState as SpeakingExercise;
|
||||
URL.revokeObjectURL(speakingExercise.video_url);
|
||||
dispatch({type: "UPDATE_SECTION_SINGLE_FIELD", payload: {
|
||||
sectionId: section.sectionId, module: "listening", field: "state", value: {...speakingExercise, video_url: undefined}
|
||||
}})
|
||||
}
|
||||
if (sectionState.type === 'interactiveSpeaking') {
|
||||
const interactiveSpeaking = sectionState as InteractiveSpeakingExercise;
|
||||
interactiveSpeaking.prompts.forEach(prompt => {
|
||||
URL.revokeObjectURL(prompt.video_url);
|
||||
});
|
||||
dispatch({type: "UPDATE_SECTION_SINGLE_FIELD", payload: {
|
||||
sectionId: section.sectionId, module: "listening", field: "state", value: {
|
||||
...interactiveSpeaking, prompts: interactiveSpeaking.prompts.map((p)=> ({...p, video_url: undefined}))
|
||||
}
|
||||
}})
|
||||
}
|
||||
});
|
||||
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
|
||||
return (
|
||||
@@ -60,7 +103,7 @@ export default function Generation({ user }: { user: User; }) {
|
||||
placeholder="Insert a title here"
|
||||
name="title"
|
||||
label="Title"
|
||||
onChange={(title) => updateRoot({title})}
|
||||
onChange={(title) => updateRoot({ title })}
|
||||
roundness="xl"
|
||||
defaultValue={title}
|
||||
required
|
||||
@@ -69,7 +112,7 @@ export default function Generation({ user }: { user: User; }) {
|
||||
<label className="font-normal text-base text-mti-gray-dim">Module</label>
|
||||
<RadioGroup
|
||||
value={currentModule}
|
||||
onChange={(currentModule) => updateRoot({currentModule})}
|
||||
onChange={(currentModule) => updateRoot({ currentModule })}
|
||||
className="flex flex-row -2xl:flex-wrap w-full gap-4 -md:justify-center justify-between">
|
||||
{[...MODULE_ARRAY].map((x) => (
|
||||
<Radio value={x} key={x}>
|
||||
|
||||
Reference in New Issue
Block a user