Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

12 uploadthing n image form #11

Open
wants to merge 12 commits into
base: 11-course-description-form
Choose a base branch
from
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
"use client";

import { zodResolver } from "@hookform/resolvers/zod";
import axios from "axios";
import { useState } from "react";
import { useForm } from "react-hook-form";
import * as z from "zod";

import FileUpload from "@/components/file-upload";
import { Button } from "@/components/ui/button";
import { Course } from "@prisma/client";
import { ImageIcon, Pencil, PlusCircle } from "lucide-react";
import Image from "next/image";
import { useRouter } from "next/navigation";
import toast from "react-hot-toast";

const formSchema = z.object({
imageUrl: z.string().min(1, { message: "Image url is required" }),
});

interface Props {
initialData: Course;
courseId: string;
}

const ImageForm = ({ initialData, courseId }: Props) => {
const router = useRouter();
const [isEditing, setEditing] = useState(false);
const toggleEdit = () => setEditing((current) => !current);

const onSubmit = async (values: z.infer<typeof formSchema>) => {
try {
await axios.patch(`/api/courses/${courseId}`, values);
toast.success("Course title updated");
toggleEdit();
router.refresh();
} catch {
toast.error("Something went wrong");
}
};

return (
<div className="mt-6 border bg-slate-100 rounded-md p-4">
<div className="font-medium flex items-center justify-between">
Course image
<Button variant="ghost" onClick={toggleEdit}>
{isEditing && <>Cancel</>}
{!isEditing && !initialData.imageUrl && (
<>
<PlusCircle className="h-4 w-4 mr-2" />
Add an image
</>
)}
{!isEditing && initialData.imageUrl && (
<>
<Pencil className="h-4 w-4 mr-2" />
Edit image
</>
)}
</Button>
</div>
{!isEditing &&
(!initialData.imageUrl ? (
<div className="flex items-center justify-center h-60 bg-slate-200 rounded-md">
<ImageIcon className="h-10 w-10 text-slate-500" />
</div>
) : (
<div className="relative aspect-video mt-2">
<Image
alt="Upload"
fill
className="object-cover rounded-md"
src={initialData.imageUrl}
/>
</div>
))}
{isEditing && (
<div>
<FileUpload
endpoint="courseImage"
onChange={(url) => {
if (url) {
onSubmit({ imageUrl: url });
}
}}
/>

<div className="text-xs text-muted-foreground mt-4">16:9 aspect ratio recommended</div>
</div>
)}
</div>
);
};

export default ImageForm;
14 changes: 5 additions & 9 deletions app/(dashboard)/(routes)/teacher/courses/[courseId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { LayoutDashboard } from "lucide-react";
import { redirect } from "next/navigation";
import TitleForm from "./_components/TitleForm";
import DescriptionForm from "./_components/DescriptionForm";
import ImageForm from "./_components/ImageForm";

type Props = {
params: {
Expand Down Expand Up @@ -52,17 +53,12 @@ const CourseIdPage = async ({ params }: Props) => {
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mt-16">
<div>
<div className="flex items-center gap-x-2">
<IconBadge icon={LayoutDashboard}/>
<IconBadge icon={LayoutDashboard} />
<h2 className="text-xl">Customize your course</h2>
</div>
<TitleForm
initialData={course}
courseId={course.id}
/>
<DescriptionForm
initialData={course}
courseId={course.id}
/>
<TitleForm initialData={course} courseId={course.id} />
<DescriptionForm initialData={course} courseId={course.id} />
<ImageForm initialData={course} courseId={course.id} />
</div>
</div>
</div>
Expand Down
24 changes: 24 additions & 0 deletions app/api/uploadthing/core.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { auth } from "@clerk/nextjs";
import { createUploadthing, type FileRouter } from "uploadthing/next";

const f = createUploadthing();

const handleAuth = () => {
const { userId } = auth();
if (!userId) throw new Error("Unauthorized");
return { userId };
};

export const ourFileRouter = {
courseImage: f({ image: { maxFileSize: "4MB", maxFileCount: 1 } })
.middleware(() => handleAuth())
.onUploadComplete(() => {}),
courseAttachment: f(["text", "image", "video", "audio", "pdf"])
.middleware(() => handleAuth())
.onUploadComplete(() => {}),
chapterVideo: f({ video: { maxFileSize: "512GB", maxFileCount: 1 } })
.middleware(() => handleAuth())
.onUploadComplete(() => {}),
} satisfies FileRouter;

export type OurFileRouter = typeof ourFileRouter;
8 changes: 8 additions & 0 deletions app/api/uploadthing/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { createNextRouteHandler } from "uploadthing/next";

import { ourFileRouter } from "./core";

// Export routes for Next App Router
export const { GET, POST } = createNextRouteHandler({
router: ourFileRouter,
});
4 changes: 3 additions & 1 deletion app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,6 @@ body,
body {
@apply bg-background text-foreground;
}
}
}

@import "~@uploadthing/react/styles.css"
22 changes: 22 additions & 0 deletions components/file-upload.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"use client";

import { UploadDropzone } from "@/lib/uploadthing";
import { ourFileRouter } from "@/app/api/uploadthing/core";
import toast from "react-hot-toast";

interface Props {
onChange: (url?: string) => void;
endpoint: keyof typeof ourFileRouter;
}

const FileUpload = ({ onChange, endpoint }: Props) => {
return (
<UploadDropzone
endpoint={endpoint}
onClientUploadComplete={(res) => onChange(res?.[0].url)}
onUploadError={(error: Error) => toast.error(`${error?.message}`)}
/>
);
};

export default FileUpload;
6 changes: 6 additions & 0 deletions lib/uploadthing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { generateComponents } from "@uploadthing/react";

import type { OurFileRouter } from "@/app/api/uploadthing/core";

export const { UploadButton, UploadDropzone, Uploader } =
generateComponents<OurFileRouter>();
2 changes: 1 addition & 1 deletion middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { authMiddleware } from "@clerk/nextjs";
// Please edit this to allow other routes to be public as needed.
// See https://clerk.com/docs/references/nextjs/auth-middleware for more information about configuring your Middleware
export default authMiddleware({
publicRoutes: ["/test"]
publicRoutes: ["/api/uploadthing"]
});

export const config = {
Expand Down
6 changes: 5 additions & 1 deletion next.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
/** @type {import('next').NextConfig} */
const nextConfig = {}
const nextConfig = {
images: {
domains: ["utfs.io"]
}
}

module.exports = nextConfig
81 changes: 81 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"@types/node": "20.10.3",
"@types/react": "18.2.42",
"@types/react-dom": "18.2.17",
"@uploadthing/react": "^6.0.2",
"autoprefixer": "10.4.16",
"axios": "^1.6.2",
"class-variance-authority": "^0.7.0",
Expand All @@ -35,6 +36,7 @@
"tailwindcss": "3.3.6",
"tailwindcss-animate": "^1.0.7",
"typescript": "5.3.2",
"uploadthing": "^6.1.0",
"zod": "^3.22.4"
},
"devDependencies": {
Expand Down
16 changes: 9 additions & 7 deletions tailwind.config.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { withUt } from "uploadthing/tw";

/** @type {import('tailwindcss').Config} */
module.exports = {
module.exports = withUt({
darkMode: ["class"],
content: [
'./pages/**/*.{ts,tsx}',
'./components/**/*.{ts,tsx}',
'./app/**/*.{ts,tsx}',
'./src/**/*.{ts,tsx}',
],
"./pages/**/*.{ts,tsx}",
"./components/**/*.{ts,tsx}",
"./app/**/*.{ts,tsx}",
"./src/**/*.{ts,tsx}",
],
theme: {
container: {
center: true,
Expand Down Expand Up @@ -73,4 +75,4 @@ module.exports = {
},
},
plugins: [require("tailwindcss-animate")],
}
});