Skip to content

Commit

Permalink
feat: upload to cloudinary
Browse files Browse the repository at this point in the history
  • Loading branch information
nguyenthanhanit committed Dec 17, 2023
1 parent 25e5550 commit b886300
Show file tree
Hide file tree
Showing 12 changed files with 755 additions and 87 deletions.
62 changes: 39 additions & 23 deletions app/admin/[slug]/new/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import 'react-toastify/dist/ReactToastify.css';
import {capitalize} from "@/utils/letter";
import DynamicComponent from "@/components/dynamic-component";
import {Spinner} from "@nextui-org/spinner";
import MultipleUploadForm from "@/components/admin/multiple-upload-form";

const FIELDS: any = {
news: [
Expand Down Expand Up @@ -65,6 +64,7 @@ const FIELDS: any = {

export default function NewPage({params}: { params: { slug: string, id: string } }) {
const [loading, setLoading] = useState(true);
const [images, setImages] = useState<any>({});

// This code inside useEffect will run when the component is mounted
useEffect(() => {
Expand All @@ -76,7 +76,16 @@ export default function NewPage({params}: { params: { slug: string, id: string }
}

fetchData().then(res => {
let resImages: any = {};
FIELDS[params.slug].forEach((field: { component: string; name: string; }) => {
if (field.component === 'SingleFileUploadForm') {
resImages[field.name] = res[field.name];
res[field.name] = res[field.name].secure_url;
}
})

reset(res)
setImages(resImages);
setLoading(false);
});
} else {
Expand All @@ -97,34 +106,41 @@ export default function NewPage({params}: { params: { slug: string, id: string }
const formData = new FormData();

FIELDS[params.slug].forEach((field: { component: string; name: string; }) => {
if (field.component === 'SingleFileUploadForm') {
if (field.component === 'SingleFileUploadForm' && typeof dataForm[field.name] === "object" && "arrayBuffer" in dataForm[field.name]) {
formData.append(field.name, dataForm[field.name]);
}
})

const resUpload = await fetch("/admin/api/upload", {
method: "POST",
body: formData,
});

const {
data,
error,
}: {
data: {
urls: {};
} | null;
error: string | null;
} = await resUpload.json();

if (error || !data) {
toast.error(error || "Sorry! something went wrong.");
return;
dataForm = {
...dataForm,
...images,
}

each(data.urls, (url, name) => {
dataForm[name] = url;
})
if (Array(formData.entries()).length > 0) {
const resUpload = await fetch("/admin/api/upload", {
method: "POST",
body: formData,
});

const {
data,
error,
}: {
data: {
urls: {};
} | null;
error: string | null;
} = await resUpload.json();

if (error || !data) {
toast.error(error || "Sorry! something went wrong.");
return;
}

each(data.urls, (url, name) => {
dataForm[name] = url ?? dataForm[name];
})
}

dataForm.publish_at = moment(dataForm.publish_at).format("x");
const res = await fetch('/admin/api', {
Expand Down
29 changes: 27 additions & 2 deletions app/admin/api/upload/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {type NextRequest, type NextResponse} from 'next/server'
import {upload} from "@/lib/upload";
import cloudinary from "@/lib/cloudinary";
import {UploadApiResponse} from "cloudinary";

export async function POST(request: NextRequest, res: NextResponse) {
const formData = await request.formData();
Expand All @@ -9,7 +10,31 @@ export async function POST(request: NextRequest, res: NextResponse) {
if (typeof pair[1] === "object" && "arrayBuffer" in pair[1]) {
const file = pair[1] as Blob | null;

urls[pair[0]] = !file ? null : await upload(file);
if (!file) {
urls[pair[0]] = null;
return;
}

const arrayBuffer = await file.arrayBuffer();
const buffer = new Uint8Array(arrayBuffer);

const results: UploadApiResponse | undefined = await new Promise((resolve, reject) => {
cloudinary.uploader.upload_stream({
resource_type: 'image'
}, function (error, result) {
if (error) {
reject(error);
return;
}
resolve(result);
}).end(buffer);
});

urls[pair[0]] = !results ? null : {
public_id: results.public_id,
url: results.url,
secure_url: results.secure_url,
};
}
}

Expand Down
16 changes: 6 additions & 10 deletions components/admin/list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const columns = [
type Props = {
data: {
_id: string
thumbnail: string
thumbnail: { public_id: string, url: string, secure_url: string },
name: string
publish_at: number
}[];
Expand Down Expand Up @@ -97,31 +97,29 @@ export default function List({data, type}: Props) {
}, [sortDescriptor, items]);

const renderCell = React.useCallback((user: User, columnKey: React.Key) => {
const cellValue = user[columnKey as keyof User];

switch (columnKey) {
case "thumbnail":
return (
<div className="flex flex-col">
{typeof cellValue === 'string' && <Image
<Image
shadow="sm"
width="100%"
alt={''}
className="w-full object-cover h-[100px] rounded-b-none"
src={cellValue}
/>}
src={user[columnKey].secure_url}
/>
</div>
);
case "name":
return (
<div className="flex flex-col">
<p className="text-bold text-small capitalize">{cellValue}</p>
<p className="text-bold text-small capitalize">{user[columnKey]}</p>
</div>
);
case "publish_at":
return (
<div className="flex flex-col">
<p className="text-bold text-small capitalize">{moment(cellValue).format('DD/MM/YYYY')}</p>
<p className="text-bold text-small capitalize">{moment(user[columnKey]).format('DD/MM/YYYY')}</p>
</div>
);
case "actions":
Expand All @@ -147,8 +145,6 @@ export default function List({data, type}: Props) {
</Button>
</div>
);
default:
return cellValue;
}
}, []);

Expand Down
7 changes: 2 additions & 5 deletions components/admin/single-upload-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,7 @@ const SingleFileUploadForm = ({label, isError, value, onChange, ...rest}: Props)

return (
<>
<form
className="w-full p-3 border border-gray-500 border-dashed"
onSubmit={(e) => e.preventDefault()}
>
<div className="w-full p-3 border border-gray-500 border-dashed">
<div className={'flex justify-between'}>
<h1 className={'m-auto ml-0 text-sm'}>{label}</h1>
<div className="order-last">
Expand Down Expand Up @@ -118,7 +115,7 @@ const SingleFileUploadForm = ({label, isError, value, onChange, ...rest}: Props)
)}
</div>
</div>
</form>
</div>
<ToastContainer/>
</>
);
Expand Down
4 changes: 2 additions & 2 deletions components/pages/news/list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ type Props = {
name: string
publish_at: number
content: string
thumbnail: string
thumbnail: { public_id: string, url: string, secure_url: string },
}[]
}

Expand All @@ -37,7 +37,7 @@ export default function List({data}: Props) {
width="100%"
alt={item.name}
className="w-full object-cover h-[200px] rounded-b-none"
src={item.thumbnail}
src={item.thumbnail.secure_url}
/>
</CardBody>
<CardFooter>
Expand Down
9 changes: 9 additions & 0 deletions lib/cloudinary.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import {v2 as cloudinary} from 'cloudinary';

cloudinary.config({
cloud_name: process.env.CLOUDINARY_NAME,
api_key: process.env.CLOUDINARY_API_KEY,
api_secret: process.env.CLOUDINARY_API_SECRET
})

export default cloudinary
37 changes: 0 additions & 37 deletions lib/upload.ts

This file was deleted.

19 changes: 17 additions & 2 deletions models/News.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,29 @@ interface INews {
name: string,
publish_at: number,
content: string,
thumbnail: string,
thumbnail: { public_id: string, url: string, secure_url: string },
}

const subDocumentSchema = new mongoose.Schema({
public_id: {
type: String,
required: true,
},
url: {
type: String,
required: true,
},
secure_url: {
type: String,
required: true,
},
});

const userSchema = new mongoose.Schema<INews>({
name: {type: String, required: true},
publish_at: {type: Number, required: true},
content: {type: String, required: true},
thumbnail: {type: String, required: true},
thumbnail: {type: subDocumentSchema, required: true},
});

const News = mongoose.models.News || mongoose.model<INews>('News', userSchema);
Expand Down
27 changes: 21 additions & 6 deletions models/Project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,33 @@ import mongoose from 'mongoose';

interface IProject {
name: string,
images: [],
images: [{ public_id: string, url: string, secure_url: string }],
content: string,
thumbnail: string,
cover: string,
thumbnail: { public_id: string, url: string, secure_url: string },
cover: { public_id: string, url: string, secure_url: string },
}

const subDocumentSchema = new mongoose.Schema({
public_id: {
type: String,
required: true,
},
url: {
type: String,
required: true,
},
secure_url: {
type: String,
required: true,
},
});

const userSchema = new mongoose.Schema<IProject>({
name: {type: String, required: true},
images: {type: [String], required: false},
images: {type: [subDocumentSchema], required: false},
content: {type: String, required: true},
thumbnail: {type: String, required: true},
cover: {type: String, required: true},
thumbnail: {type: subDocumentSchema, required: true},
cover: {type: subDocumentSchema, required: true},
});

const Project = mongoose.models.Project || mongoose.model<IProject>('Project', userSchema);
Expand Down
1 change: 1 addition & 0 deletions next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const nextConfig = {
pathname: '/images/**',
},
],
domains: ['res.cloudinary.com']
},
}

Expand Down
Loading

0 comments on commit b886300

Please sign in to comment.