Skip to content

Commit

Permalink
feat: cloudinary support for uploads (#48)
Browse files Browse the repository at this point in the history
Co-authored-by: moonlitgrace <[email protected]>
  • Loading branch information
daaniissh and moonlitgrace authored Sep 12, 2024
1 parent a59f486 commit f171e5e
Show file tree
Hide file tree
Showing 10 changed files with 226 additions and 804 deletions.
7 changes: 6 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ DATABASE_URL=
# SECRET
SECRET_KEY=

# ADMIN CREDENTIALS
# ADMIN CREDS
ADMIN_USERNAME=
ADMIN_PASSWORD=

# CLOUDNINARY CREDS
CLOUDINARY_CLOUD_NAME=
CLOUDINARY_API_KEY=
CLOUDINARY_API_SECRET=
5 changes: 5 additions & 0 deletions .env.local.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,8 @@ SECRET_KEY=
# ADMIN CREDENTIALS
ADMIN_USERNAME=
ADMIN_PASSWORD=

# CLOUDNINARY CREDS
CLOUDINARY_CLOUD_NAME=
CLOUDINARY_API_KEY=
CLOUDINARY_API_SECRET=
24 changes: 22 additions & 2 deletions actions/admin.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { validateFile } from '@/lib/utils';
import { AdminBlogFormState, AdminBlogSchema } from '@/zod_schemas/admin';

export default async function adminBlogSubmit(state: AdminBlogFormState, formData: FormData) {
console.log(state);

const validatedFields = AdminBlogSchema.safeParse({
id: formData.get('id') ? Number(formData.get('id')) : undefined,
title: formData.get('title'),
Expand All @@ -18,6 +17,27 @@ export default async function adminBlogSubmit(state: AdminBlogFormState, formDat
}

try {
let coverImage = formData.get('cover') as File;
// coverImage returns invalid File object if nothing is selected
if (validateFile(coverImage)) {
const imageData = new FormData();
imageData.append('file', coverImage);

const imgRes = await fetch('/api/cloudinary', {
method: 'POST',
body: imageData,
});

if (!imgRes.ok) {
const imgResErr = await imgRes.json();
console.error('Image upload failed:', imgResErr);
return { message: imgResErr };
}

const data = await imgRes.json();
validatedFields.data.cover = data.url;
}

const res = await fetch('/api/blog', {
method: 'POST',
body: JSON.stringify(validatedFields.data),
Expand Down
25 changes: 25 additions & 0 deletions app/api/cloudinary/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { NextResponse } from 'next/server';
import cloudinary from '@/lib/cloudinary';
import { validateFile } from '@/lib/utils';

export async function POST(req: Request) {
try {
const formData = await req.formData();
const file = formData.get('file') as File;

if (!validateFile(file)) {
return NextResponse.json({ message: 'No file provided' }, { status: 400 });
}

const buffer = await file.arrayBuffer();
const base64String = Buffer.from(buffer).toString('base64');
const base64Image = `data:${file.type};base64,${base64String}`;

const uploadRes = await cloudinary.uploader.upload(base64Image);

return NextResponse.json({ url: uploadRes.url });
} catch (error) {
if (error instanceof Error)
return NextResponse.json({ message: error.message }, { status: 500 });
}
}
10 changes: 8 additions & 2 deletions components/admin/AdminBlogForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ type Props = {
export default function AdminBlogForm({ id, title = '', tag = '', content = '', cover }: Props) {
const [state, action] = useFormState(adminBlogSubmit, undefined);
const [contentState, setContentState] = useState(content);

const router = useRouter();

useEffect(() => {
Expand All @@ -38,6 +39,9 @@ export default function AdminBlogForm({ id, title = '', tag = '', content = '',
<form className="flex flex-col gap-3" action={action}>
{/* hidden fields */}
<input type="hidden" name="id" value={id?.toString() || ''} />
{/* This will send if submit form with preview tab open */}
{/* because then there will be no input with name attr */}
<input type="hidden" name="content" value={contentState} />

<div className="grid w-full items-center gap-1.5">
<Input
Expand Down Expand Up @@ -74,7 +78,7 @@ export default function AdminBlogForm({ id, title = '', tag = '', content = '',
<TabsContent value="content">
<Textarea
className="h-40"
placeholder="Type content here."
placeholder="Type content here..."
id="content"
name="content"
value={contentState}
Expand All @@ -93,7 +97,9 @@ export default function AdminBlogForm({ id, title = '', tag = '', content = '',
</Tabs>
</div>
<div className="flex gap-3">
<Button variant="secondary">Draft</Button>
<Button type="button" variant="secondary">
Draft
</Button>
<SubmitButton />
</div>
</form>
Expand Down
3 changes: 3 additions & 0 deletions environment.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,8 @@ namespace NodeJS {
SECRET_KEY: string;
ADMIN_USERNAME: string;
ADMIN_PASSWORD: string;
CLOUDINARY_CLOUD_NAME: string;
CLOUDINARY_API_KEY: string;
CLOUDINARY_API_SECRET: string;
}
}
10 changes: 10 additions & 0 deletions lib/cloudinary.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { v2 as cloudinary } from 'cloudinary';

cloudinary.config({
cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
api_key: process.env.CLOUDINARY_API_KEY,
api_secret: process.env.CLOUDINARY_API_SECRET,
secure: true,
});

export default cloudinary;
4 changes: 4 additions & 0 deletions lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,7 @@ export function formatDate(dateStr: Date) {
export function escapeText(text: string) {
return text.toLowerCase().replace(/[^\w]+/g, '-');
}

export function validateFile(file: File) {
return file instanceof File && file.name !== '' && file.size > 0;
}
Loading

0 comments on commit f171e5e

Please sign in to comment.