Moved the upload folder out of public, an api endpoint to server the files, and a middleware to make it seemlessly pull the uploads from the api without api in the name. Further styling on the expand/collapse podcast card logic to have it show new lines and to fade the collapsed paragraph. Publish page now shows that it's 'loading' in the submit button and alerts the user when done. After success upload it'll redirect to the podcasts listing page.

This commit is contained in:
Justin Walrath 2025-05-12 11:26:02 -04:00
parent 8e9ef164a9
commit 5559e85588
7 changed files with 56 additions and 44 deletions

2
.gitignore vendored
View File

@ -19,7 +19,7 @@
# production # production
/build /build
/public/uploads/ /uploads/
db.json db.json
# misc # misc

View File

@ -25,11 +25,13 @@ export default function PodcastCard({
<div className="p-4 flex-grow flex flex-col bg-purple-200"> <div className="p-4 flex-grow flex flex-col bg-purple-200">
<h2 className="text-lg font-bold text-gray-800">{title}</h2> <h2 className="text-lg font-bold text-gray-800">{title}</h2>
<div <div className="relative">
className={`text-sm text-gray-600 overflow-hidden transition-all duration-300 ${descriptionExpanded ? 'max-h-full' : 'max-h-[30px]' <div className={`text-sm text-gray-600 overflow-hidden transition-all duration-300 whitespace-pre-wrap ${descriptionExpanded ? 'max-h-full' : 'max-h-[60px]'}`}>
}`} {description}
> </div>
{description} {!descriptionExpanded && (
<div className="absolute bottom-0 left-0 w-full h-6 bg-gradient-to-t from-purple-200 to-transparent pointer-events-none"></div>
)}
</div> </div>
<button onClick={toggleExpand} className="mt-2 text-center text-blue-500 hover:underline text-sm self-center"> <button onClick={toggleExpand} className="mt-2 text-center text-blue-500 hover:underline text-sm self-center">
{descriptionExpanded ? 'Show Less' : 'Show More'} {descriptionExpanded ? 'Show Less' : 'Show More'}

View File

@ -1,6 +1,8 @@
"use client"; "use client";
import { useForm, SubmitHandler } from "react-hook-form"; import { useForm, SubmitHandler } from "react-hook-form";
import { useRouter } from "next/navigation";
import { useState } from "react";
type PodcastDto = { type PodcastDto = {
imageUrl?: FileList; imageUrl?: FileList;
@ -12,9 +14,12 @@ type PodcastDto = {
}; };
export default function StreamPublishForm({ stream }: { stream?: string | string[] }) { export default function StreamPublishForm({ stream }: { stream?: string | string[] }) {
const { register, handleSubmit, formState: { errors } } = useForm<PodcastDto>(); const { register, handleSubmit, formState: { errors } } = useForm<PodcastDto>(),
router = useRouter(),
[submitting, setSubmitting] = useState(false);
const onSubmit: SubmitHandler<PodcastDto> = (data) => { const onSubmit: SubmitHandler<PodcastDto> = (data) => {
setSubmitting(true);
const formData = new FormData(); const formData = new FormData();
formData.append("image", data.imageUrl?.[0] as File); formData.append("image", data.imageUrl?.[0] as File);
formData.append("title", data.title); formData.append("title", data.title);
@ -29,10 +34,14 @@ export default function StreamPublishForm({ stream }: { stream?: string | string
body: formData, body: formData,
}) })
.then((res) => res.json()) .then((res) => res.json())
.then((result) => { .then(() => {
console.log("Podcast created:", result); alert(`Podcast uploaded: ${data.title}`);
router.push(`/${stream}/podcasts`);
}) })
.catch((err) => console.error("Error creating podcast:", err)); .catch((err) => {
alert(`Error creating podcast: JSON.string${err}`);
setSubmitting(false);
});
}; };
return ( return (
@ -102,8 +111,9 @@ export default function StreamPublishForm({ stream }: { stream?: string | string
<button <button
type="submit" type="submit"
className="w-full bg-purple-600 text-purple-200 py-2 px-4 rounded-md hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2" className="w-full bg-purple-600 text-purple-200 py-2 px-4 rounded-md hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2"
disabled={submitting}
> >
Submit {submitting ? 'Uploading...' : 'Upload'}
</button> </button>
</form> </form>
</div> </div>

View File

@ -21,7 +21,7 @@ export const convertUrlToPublic = (url?: string) => {
}; };
export const preparePodcastItem = (podcast: PodcastDto) => { export const preparePodcastItem = (podcast: PodcastDto) => {
const fileSize = getFileSize(path.join(process.cwd(), `public/uploads/${podcast.url}`)), const fileSize = getFileSize(path.join(process.cwd(), `uploads/${podcast.url}`)),
fileUrl = convertUrlToPublic(podcast.url); fileUrl = convertUrlToPublic(podcast.url);
return { return {

15
src/middleware.ts Normal file
View File

@ -0,0 +1,15 @@
import { NextRequest, NextResponse } from 'next/server';
export function middleware(req: NextRequest) {
const basePath = process.env.NEXT_PUBLIC_BASE_PATH || '';
const url = req.nextUrl.clone();
const adjustedPathname = url.pathname.replace(basePath, '');
if (adjustedPathname.startsWith('/uploads/')) {
url.pathname = `/api${adjustedPathname}`;
return NextResponse.rewrite(url);
}
return NextResponse.next();
}

View File

@ -27,7 +27,7 @@ const post = async (req: NextApiRequest, res: NextApiResponse) => {
return res.status(400).json({ error: 'Invalid stream parameter' }); return res.status(400).json({ error: 'Invalid stream parameter' });
} }
const uploadDir = path.join(process.cwd(), 'public/uploads'); const uploadDir = path.join(process.cwd(), 'uploads');
await fs.mkdir(uploadDir, { recursive: true }); await fs.mkdir(uploadDir, { recursive: true });
const form = new multiparty.Form({ const form = new multiparty.Form({
uploadDir uploadDir
@ -80,37 +80,6 @@ const post = async (req: NextApiRequest, res: NextApiResponse) => {
res.status(500).json({ error: 'Internal server error' }); res.status(500).json({ error: 'Internal server error' });
} }
}); });
// try {
// const { title, description, author, image, file } = req.body;
// const uploadDir = path.join(process.cwd(), 'public/uploads');
// const imageFileName = `${uuidv4()}${path.extname(image.name)}`;
// const imageFilePath = path.join(uploadDir, imageFileName);
// await fs.writeFile(imageFilePath, Buffer.from(image.data, 'base64'));
// const podcastFileName = `${uuidv4()}${path.extname(file.name)}`;
// const podcastFilePath = path.join(uploadDir, podcastFileName);
// await fs.writeFile(podcastFilePath, Buffer.from(file.data, 'base64'));
// const podcastData: PodcastDto = {
// podcastId: uuidv4(),
// streamId: stream,
// title,
// description,
// uploadDate: Date.now().toString(),
// author,
// imageUrl: imageFileName,
// url: podcastFileName,
// };
// await publishPodcast(podcastData);
// res.status(201).json({ message: 'Podcast created successfully' });
// } catch (error) {
// console.error('Error handling request:', error);
// res.status(500).json({ error: 'Internal server error' });
// }
}; };
export default function handler( export default function handler(

View File

@ -0,0 +1,16 @@
import { NextApiRequest, NextApiResponse } from 'next';
import path from 'path';
import fs from 'fs';
export default function handler(req: NextApiRequest, res: NextApiResponse) {
const { file } = req.query;
const filePath = path.join(process.cwd(), 'uploads', file as string);
if (!fs.existsSync(filePath)) {
return res.status(404).json({ error: 'File not found' });
}
res.setHeader('Content-Type', 'application/octet-stream');
fs.createReadStream(filePath).pipe(res);
}