Skip to content

Commit eba9e9d

Browse files
Add cloudinary service for media
1 parent 247728b commit eba9e9d

14 files changed

+325
-107
lines changed

README.md

+1-2
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,4 @@ This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-opti
2222

2323
Features to be added on priority:
2424

25-
1. Cloudinary or any other media service for media upload and storing the data instead of storing it on our own server
26-
2. User to add images in the notes apart from profile
25+
1. User to add images in the notes apart from profile

app/actions/update-profile-pic.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
import prisma from '@/lib/db';
44
import { revalidatePath } from 'next/cache';
55

6-
export async function updateProfilePic(userId: string, filename: string) {
6+
export async function updateProfilePic(userId: string, profilePicUrl: string) {
77
try {
8-
const updatedUser = await prisma.user.update({
8+
await prisma.user.update({
99
where: { id: userId },
10-
data: { profilePic: filename },
10+
data: { profilePic: profilePicUrl },
1111
});
1212
revalidatePath('/profile');
1313
return { success: true };

app/api/upload/route.ts

+12-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { writeFile, mkdir } from 'fs/promises';
1+
import { writeFile, mkdir, unlink } from 'fs/promises';
22
import { NextRequest, NextResponse } from 'next/server';
33
import path from 'path';
4+
import cloudinary from '@/lib/cloudinary';
45

56
export async function POST(request: NextRequest) {
67
try {
@@ -21,9 +22,18 @@ export async function POST(request: NextRequest) {
2122
// Create the uploads directory if it doesn't exist
2223
await mkdir(uploadDir, { recursive: true });
2324

25+
// Temporarily store the file
2426
await writeFile(filepath, buffer);
2527

26-
return NextResponse.json({ success: true, filename });
28+
// Upload to Cloudinary
29+
const result = await cloudinary.uploader.upload(filepath, {
30+
folder: 'profile-pictures',
31+
});
32+
33+
// Delete the temporary file
34+
await unlink(filepath);
35+
36+
return NextResponse.json({ success: true, filename: result.secure_url });
2737
} catch (error) {
2838
console.error('Upload error:', error);
2939
return NextResponse.json({

app/layout.tsx

+10-1
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,23 @@ import './globals.css';
44
import { ThemeProvider } from 'next-themes';
55
import { ModeToggle } from '@/components/mode-toggle';
66
import { Toaster } from '@/components/ui/toaster';
7+
// import { useServerSession } from '@/lib/useServerSession';
78

89
const inter = Inter({ subsets: ['latin'] });
910

1011
export const metadata: Metadata = {
1112
title: 'YOUR NOTES',
12-
description: 'Generated by us',
13+
description: 'Generated by you',
1314
};
1415

16+
// export async function generateMetadata(): Promise<Metadata> {
17+
// const { user } = await useServerSession();
18+
19+
// return {
20+
// title: user.username,
21+
// };
22+
// }
23+
1524
export default async function RootLayout({
1625
children,
1726
}: Readonly<{

app/profile/page.tsx

+13-8
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,22 @@ export default async function ProfilePage() {
1818

1919
return (
2020
<div className="flex flex-col items-center justify-center p-10">
21-
<h1>Profile</h1>
21+
<h1 className="m-4">Profile</h1>
2222
{UserDetails?.profilePic && (
23-
<Image
24-
src={`/uploads/${UserDetails.profilePic}`}
25-
alt="Profile Picture"
26-
width={200}
27-
height={200}
28-
/>
23+
<div className="rounded-full border-violet-600 border-8 overflow-hidden w-40 h-40 mb-4">
24+
<Image
25+
src={UserDetails.profilePic}
26+
alt="Profile Picture"
27+
className="w-full h-full object-cover"
28+
layout="lazy"
29+
height={200}
30+
width={200}
31+
/>
32+
</div>
2933
)}
34+
3035
<ProfilePicUpload userId={user.id} />
31-
<p>Username: {user.username}</p>
36+
<p className="p-4">Username: {user.username}</p>
3237
<p>
3338
Name: {UserDetails?.firstName} {UserDetails?.lastName}
3439
</p>

components/create-todo.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export default function CreateTodo({
1313
const { toast } = useToast();
1414

1515
async function action(formData: FormData) {
16-
const res = await createTodo(formData, user.id);
16+
const res = await createTodo(formData, user.id, '');
1717
formRef.current?.reset();
1818
toast({
1919
title: res.error ? 'Uh oh! Something went wrong.' : 'success',

components/profile-pic-upload.tsx

+26-11
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,18 @@
33
import { useState } from 'react';
44
import { updateProfilePic } from '@/app/actions/update-profile-pic';
55
import { useToast } from '@/components/ui/use-toast';
6+
import { Button } from '@/components/ui/button';
67

78
export default function ProfilePicUpload({ userId }: { userId: string }) {
89
const [file, setFile] = useState<File | null>(null);
9-
const [error, setError] = useState<string | null>(null);
10+
const [loading, setLoading] = useState<boolean>(false);
11+
1012
const { toast } = useToast();
1113

1214
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
1315
e.preventDefault();
14-
setError(null);
1516
if (!file) return;
17+
setLoading(true);
1618

1719
const formData = new FormData();
1820
formData.append('file', file);
@@ -31,6 +33,7 @@ export default function ProfilePicUpload({ userId }: { userId: string }) {
3133

3234
if (data.success) {
3335
console.log('File uploaded successfully:', data.filename);
36+
setFile(null);
3437
const result = await updateProfilePic(userId, data.filename);
3538
console.log('Update profile pic result:', result);
3639
if (result.success) {
@@ -45,24 +48,36 @@ export default function ProfilePicUpload({ userId }: { userId: string }) {
4548
}
4649
} catch (err) {
4750
console.error('Error:', err);
48-
setError(
49-
err instanceof Error ? err.message : 'An unknown error occurred'
50-
);
51+
toast({
52+
title: err instanceof Error ? err.message : 'An unknown error occurred',
53+
variant: 'destructive',
54+
});
55+
} finally {
56+
setLoading(false);
5157
}
5258
};
5359

5460
return (
55-
<form onSubmit={handleSubmit}>
61+
<form onSubmit={handleSubmit} className="space-y-4">
5662
<input
5763
type="file"
5864
accept="image/*"
5965
onChange={e => setFile(e.target.files?.[0] || null)}
60-
className="cursor-pointer rounded border border-sky-600"
66+
className="cursor-pointer block w-full text-sm text-gray-500
67+
file:mr-4 file:py-2 file:px-4
68+
file:rounded-full file:border-0
69+
file:text-sm file:font-semibold
70+
file:bg-violet-50 file:text-violet-700
71+
hover:file:bg-violet-100"
6172
/>
62-
<button type="submit" disabled={!file}>
63-
Upload Profile Picture
64-
</button>
65-
{error && <p style={{ color: 'red' }}>{error}</p>}
73+
<Button
74+
type="submit"
75+
disabled={!file || loading}
76+
className="w-full bg-violet-600 text-white py-4 rounded-lg
77+
hover:bg-violet-700 disabled:opacity-50"
78+
>
79+
{loading ? 'Uploading...' : 'Upload Profile Picture'}
80+
</Button>
6681
</form>
6782
);
6883
}

components/ui/input.tsx

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import * as React from "react"
2+
3+
import { cn } from "@/lib/utils"
4+
5+
export interface InputProps
6+
extends React.InputHTMLAttributes<HTMLInputElement> {}
7+
8+
const Input = React.forwardRef<HTMLInputElement, InputProps>(
9+
({ className, type, ...props }, ref) => {
10+
return (
11+
<input
12+
type={type}
13+
className={cn(
14+
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
15+
className
16+
)}
17+
ref={ref}
18+
{...props}
19+
/>
20+
)
21+
}
22+
)
23+
Input.displayName = "Input"
24+
25+
export { Input }

lib/cloudinary.ts

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// lib/cloudinary.ts
2+
import { v2 as cloudinary } from 'cloudinary';
3+
4+
cloudinary.config({
5+
cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
6+
api_key: process.env.CLOUDINARY_API_KEY,
7+
api_secret: process.env.CLOUDINARY_API_SECRET,
8+
});
9+
10+
export default cloudinary;

next.config.mjs

+11-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,14 @@
11
/** @type {import('next').NextConfig} */
2-
const nextConfig = {};
2+
const nextConfig = {
3+
images: {
4+
remotePatterns: [
5+
{
6+
protocol: 'https',
7+
hostname: 'res.cloudinary.com',
8+
pathname: '**',
9+
},
10+
],
11+
},
12+
};
313

414
export default nextConfig;

package-lock.json

+58
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"@radix-ui/react-toast": "^1.2.1",
1919
"bcryptjs": "^2.4.3",
2020
"class-variance-authority": "^0.7.0",
21+
"cloudinary": "^2.4.0",
2122
"clsx": "^2.1.1",
2223
"lucide-react": "^0.414.0",
2324
"next": "14.2.5",
-855 KB
Binary file not shown.

0 commit comments

Comments
 (0)