diff --git a/app/(app)/uploadvideo/page.tsx b/app/(app)/uploadvideo/page.tsx index 2edc845..3f8f5f5 100644 --- a/app/(app)/uploadvideo/page.tsx +++ b/app/(app)/uploadvideo/page.tsx @@ -1,6 +1,5 @@ "use client" -import { useRouter } from "next/navigation"; -import React, { useState } from "react"; +import React, { useState } from "react"; import axios from "axios"; import { formData } from "@/types/interfaces"; @@ -11,14 +10,11 @@ const UploadVideo = () => { title: "", description: "", }); - const [isUploading, setIsUploading] = useState(false); - const router = useRouter(); - const MAX_FILE_SIZE = 70 * 1024 * 1024; - const handleSubmit = async (e: React.FormEvent) => { + const handleSubmit = async (e: React.SubmitEvent) => { e.preventDefault(); if (!formData.file) return; diff --git a/app/api/upload-video/route.ts b/app/api/upload-video/route.ts index 865a96c..374f821 100644 --- a/app/api/upload-video/route.ts +++ b/app/api/upload-video/route.ts @@ -1,38 +1,68 @@ import { NextRequest, NextResponse } from "next/server"; +import { auth } from "@clerk/nextjs/server"; import { prisma } from "@/lib/prisma"; import { videoOptions } from "@/utils/constants"; -import { requireUser, uploadToCloudinary } from "@/utils/helpers"; +import { uploadToCloudinary } from "@/utils/helpers"; export async function POST(request: NextRequest) { try { - const userId = await requireUser(); + // Authenticate user + const { userId } = await auth(); + if (!userId) { + return NextResponse.json( + { + error: "Unauthorized", + message: "You must be logged in to upload videos", + }, + { status: 401 } + ); + } + + // Parse upload body const formData = await request.formData(); + const file = formData.get("file") as File | null; const title = formData.get("title") as string; const description = formData.get("description") as string; const originalSize = formData.get("originalSize") as string; if (!file) { - return NextResponse.json({ error: "File not found" }, { status: 400 }); + return NextResponse.json( + { error: "File not found" }, + { status: 400 } + ); } + + // Upload to Cloudinary const result = await uploadToCloudinary(file, videoOptions); + // Save video const video = await prisma.video.create({ data: { Title: title, - description: description, + description, publicId: result.public_id, - originalSize: originalSize, + originalSize, compressedSize: String(result.bytes), - duration: result.duration || 0, + duration: result.duration?.toString() || "0", + + // If your schema has user relation: + // userId: userId }, }); return NextResponse.json(video); + } catch (error) { - console.log("Upload Video failed", error); - return NextResponse.json({ error: "Upload Video failed" }, { status: 500 }); + console.error("Upload Video failed", error); + + return NextResponse.json( + { + error: "Upload Video failed", + }, + { status: 500 } + ); } -} +} \ No newline at end of file diff --git a/app/page.tsx b/app/page.tsx index 763f341..ede5215 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -2,17 +2,10 @@ import React, { useEffect, useRef, useState } from "react"; import { CldImage } from "next-cloudinary"; +import { socialFormats } from "@/utils/constants"; type SocialFormat = keyof typeof socialFormats; -const socialFormats = { - "Instagram Square (1:1)": { width: 1080, height: 1080, aspectRatio: "1:1" }, - "Instagram Protrait (4:5)": { width: 1080, height: 1350, aspectRatio: "4:5" }, - "Twitter Post (16:9)": { width: 1200, height: 675, aspectRatio: "16:9" }, - "Twtter Header (3:1)": { width: 1500, height: 500, aspectRatio: "3:1" }, - "Facebook Cover (205:78)": { width: 820, height: 312, aspectRatio: "205:78" }, -}; - export default function Home() { const [uploadImage, setUploadImage] = useState(null); const [format, setFormat] = useState("Instagram Square (1:1)"); diff --git a/components/videoCard.tsx b/components/videoCard.tsx new file mode 100644 index 0000000..df61ea4 --- /dev/null +++ b/components/videoCard.tsx @@ -0,0 +1,145 @@ +import dayjs from "dayjs"; +import relativeTime from "dayjs/plugin/relativeTime"; +import { getCldImageUrl, getCldVideoUrl } from "next-cloudinary"; +import { useCallback, useEffect, useState } from "react"; +import { filesize } from "filesize"; +import { Clock, Download, FileDown, FileUp } from "lucide-react"; + +import { VideoProps } from "@/types/interfaces"; + +dayjs.extend(relativeTime); + +const VideoCard: React.FC = ({ video, onDownload }) => { + const [isHover, setIsHover] = useState(false); + const [previewError, setpreviewError] = useState(false); + + const getThumbnailUrl = useCallback((publicId: string) => { + return getCldImageUrl({ + src: publicId, + width: 400, + height: 255, + crop: "fill", + gravity: "auto", + format: "jpg", + quality: "auto", + assetType: "video", + }); + }, []); + + const getFullVideoUrl = useCallback((publicId: string) => { + return getCldVideoUrl({ + src: publicId, + width: 1920, + height: 1080, + }); + }, []); + + const getPreviewVideoUrl = useCallback((publicId: string) => { + return getCldVideoUrl({ + src: publicId, + width: 400, + height: 225, + rawTransformations: ["e_preview: duration_15:max_seg_9:min_seg_dur_1"], + }); + }, []); + + const formatSize = useCallback((size: number) => { + return filesize(size); + }, []); + + const formatDuration = useCallback((seconds: number) => { + const minutes = Math.floor(seconds / 60); + const remainingSeconds = Math.round(seconds % 6); + + return `${minutes}:${remainingSeconds.toString().padStart(2, "0")}`; + }, []); + + const compressionPercentage = Math.round( + (1 - Number(video.compressedSize) / Number(video.originalSize)) * 100, + ); + + useEffect(() => { + setpreviewError(false); + }, [isHover]); + + const handlePreviewError = () => { + setpreviewError(true); + }; + + return ( + <> +
setIsHover(true)} + onMouseLeave={() => setIsHover(false)} + > +
+ {isHover ? ( + previewError ? ( +
+

Preview not available

+
+ ) : ( +
+
+

{video.Title}

+ +

+ Uploaded {dayjs(video.createdAt).fromNow()} +

+ +
+
+ +
+
Original
+
{formatSize(Number(video.originalSize))}
+
+
+
+ +
+
+
Compressed
+
{formatSize(Number(video.compressedSize))}
+
+
+
+
+
+ Compression: {" "} + {compressionPercentage}% +
+ + + +
+
+ + ); +}; + +export default VideoCard; diff --git a/middleware.ts b/middleware.ts index c24e698..bf6d926 100644 --- a/middleware.ts +++ b/middleware.ts @@ -1,4 +1,4 @@ -import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server"; +import { clerkMiddleware, createRouteMatcher,} from "@clerk/nextjs/server"; import { NextResponse } from "next/server"; const isPublicRoutes = createRouteMatcher([ @@ -13,7 +13,7 @@ const isPublicRoutes = createRouteMatcher([ "/privacy", ]); -const isPublicApiRoutes = createRouteMatcher(["/api/videos"]); +const isPublicApiRoutes = createRouteMatcher(["/api/uploadvideo"]); const isAdminRoute = createRouteMatcher([ "/admin(.*)", @@ -21,8 +21,14 @@ const isAdminRoute = createRouteMatcher([ "/dashboard/analytics", ]); -export default clerkMiddleware((auth, req) => { - const { userId } = auth; +export default clerkMiddleware(async (auth, req) => { + +if (req.nextUrl.pathname === "/api/upload-video") { + return NextResponse.next(); + } + + const {userId} = await auth(); + const currentUrl = new URL(req.url); const isHomePage = currentUrl.pathname === "/home"; const isApiReq = currentUrl.pathname.startsWith("/api"); @@ -34,15 +40,15 @@ export default clerkMiddleware((auth, req) => { } //If user is not an admin then redirect to the "/signin" route. - if (userId.role !== "admin") { - return NextResponse.redirect(new URL("/sign-in", req.url)); - } + // if (userId.role !== "admin") { + // return NextResponse.redirect(new URL("/sign-in", req.url)); + // } } //If user is LoggedIn and accessing the public route and this route is not a home then user will be go to the home route first then user can change it - if (userId && isPublicRoutes(req) && !isHomePage) { - return NextResponse.redirect(new URL("/home", req.url)); - } + // if (userId && isPublicRoutes(req) && !isHomePage) { + // return NextResponse.redirect(new URL("/home", req.url)); + // } //If the User is not LoggedIn if (!userId) { @@ -69,7 +75,7 @@ export default clerkMiddleware((auth, req) => { export const config = { matcher: [ - "/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)", + "/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)", "/(api|trpc)(.*)", "/__clerk/(.*)", ], diff --git a/next.config.ts b/next.config.ts index 306b718..412a62e 100644 --- a/next.config.ts +++ b/next.config.ts @@ -23,4 +23,16 @@ const nextConfig: NextConfig = { }, }; +module.exports = { + experimental: { + serverActions: { + bodySizeLimit: '70mb', + proxyClientMaxBodySize: '70mb', + middlewareClientMaxBodySize: 100 * 1024 * 1024, + }, + }, +} + + + export default nextConfig; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index a561c17..8a61e69 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,8 +15,10 @@ "cloudinary": "^2.10.0", "clsx": "^2.1.1", "daisyui": "^5.6.14", + "dayjs": "^1.11.21", "dotenv": "^17.4.2", - "lucide-react": "^1.23.0", + "filesize": "^11.0.22", + "lucide-react": "^1.28.0", "next": "^15.3.8", "next-cloudinary": "^6.17.5", "pg": "^8.22.0", @@ -5590,6 +5592,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/dayjs": { + "version": "1.11.21", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz", + "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==", + "license": "MIT" + }, "node_modules/debounce-fn": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/debounce-fn/-/debounce-fn-4.0.0.tgz", @@ -6930,6 +6938,15 @@ "node": ">=16.0.0" } }, + "node_modules/filesize": { + "version": "11.0.22", + "resolved": "https://registry.npmjs.org/filesize/-/filesize-11.0.22.tgz", + "integrity": "sha512-RlCVs9CY+oSsRnNZn95J9vDXjNjOwddKyTFjOYtA4yxYVIxBnwiVVGJX+TFhsmu3uUf81JDGyijtYL9xgawlTw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 10.8.0" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -8891,9 +8908,9 @@ } }, "node_modules/lucide-react": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.23.0.tgz", - "integrity": "sha512-38BpJcD0JhFosxHApP/BYsBetLpQFRoTRzEzstM/XCc3jsAG7wqaY1lgVwxiUe3xqYE+lNxo2PkCmYwXWrwwIw==", + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.28.0.tgz", + "integrity": "sha512-fARAFJULsGuDDydjp6+6blekG/sBIM29TerzLjc9bQUKAcEfrSc4ZQKb25KRz4OMKd87cZTb5dgq0w/T6KufVg==", "license": "ISC", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" diff --git a/package.json b/package.json index c1229c5..38c390b 100644 --- a/package.json +++ b/package.json @@ -17,8 +17,10 @@ "cloudinary": "^2.10.0", "clsx": "^2.1.1", "daisyui": "^5.6.14", + "dayjs": "^1.11.21", "dotenv": "^17.4.2", - "lucide-react": "^1.23.0", + "filesize": "^11.0.22", + "lucide-react": "^1.28.0", "next": "^15.3.8", "next-cloudinary": "^6.17.5", "pg": "^8.22.0", diff --git a/types/interfaces.ts b/types/interfaces.ts index 003b7eb..550026e 100644 --- a/types/interfaces.ts +++ b/types/interfaces.ts @@ -1,3 +1,5 @@ +import { Video } from "@/generated/prisma/client"; + export interface CloudinaryUploadResult { public_id: string; bytes: number; @@ -6,7 +8,12 @@ export interface CloudinaryUploadResult { } export interface formData { - file: File | null, - title: string, - description: string, + file: File | null; + title: string; + description: string; +} + +export interface VideoProps { + video: Video; + onDownload: (url:string,title:string) => void; } \ No newline at end of file diff --git a/utils/constants.ts b/utils/constants.ts index 1d88ec0..bf1a4b9 100644 --- a/utils/constants.ts +++ b/utils/constants.ts @@ -12,3 +12,11 @@ export const videoOptions: UploadApiOptions = { }, ], }; + +export const socialFormats = { + "Instagram Square (1:1)": { width: 1080, height: 1080, aspectRatio: "1:1" }, + "Instagram Protrait (4:5)": { width: 1080, height: 1350, aspectRatio: "4:5" }, + "Twitter Post (16:9)": { width: 1200, height: 675, aspectRatio: "16:9" }, + "Twtter Header (3:1)": { width: 1500, height: 500, aspectRatio: "3:1" }, + "Facebook Cover (205:78)": { width: 820, height: 312, aspectRatio: "205:78" }, +}; diff --git a/utils/helpers.ts b/utils/helpers.ts index e95c761..352919c 100644 --- a/utils/helpers.ts +++ b/utils/helpers.ts @@ -6,8 +6,6 @@ import { CloudinaryUploadResult } from "@/types/interfaces"; export const requireUser = async () => { const { userId } = await auth(); - - console.log(userId); if (!userId) { throw new Error("Unauthorized");