Skip to content

Commit

Permalink
Add limited imgupld functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
Draikth committed Jul 25, 2024
1 parent fd8e018 commit 682071c
Show file tree
Hide file tree
Showing 12 changed files with 353 additions and 17 deletions.
1 change: 0 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ PGHOST=localhost
PGDATABASE=xxxxxxxxxxxxxx
PGUSERNAME=xxxxxxxxxxxxxx
PGPASSWORD=xxxxxxxxxxxxxx

NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME=xxxxxx
NEXT_PUBLIC_CLOUDINARY_API_KEY=xxxxxx
CLOUDINARY_API_SECRET=xxxxxx
Expand Down
100 changes: 89 additions & 11 deletions app/api/imageUpload/route.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,96 @@
import { v2 as cloudinary } from 'cloudinary';
import { NextRequest, NextResponse } from 'next/server';
import { createImageInsecure } from '../../../database/imgQueries';

// eslint-disable-next-line no-restricted-syntax
export async function POST(request: NextRequest): Promise<NextResponse> {
const body = (await request.json()) as {
paramsToSign: Record<string, string>;
};
export type ImageUploadResponsePost =
| {
imageUrl: string;
}
| {
error: string;
};

const { paramsToSign } = body;
type CloudinaryResponse = {
secure_url: string;
};

const signature = cloudinary.utils.api_sign_request(
paramsToSign,
process.env.CLOUDINARY_API_SECRET as string,
);
cloudinary.config({
cloud_name: process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME,
api_key: process.env.NEXT_PUBLIC_CLOUDINARY_API_KEY,
api_secret: process.env.CLOUDINARY_API_SECRET,
});

return NextResponse.json({ signature });
export async function POST(
request: NextRequest,
): Promise<NextResponse<ImageUploadResponsePost>> {
try {
const formData = await request.formData();
const file = formData.get('image') as File;

if (!file.name) {
return NextResponse.json({ error: 'Please select an image' });
}

if (file.size > 1024 * 1024 * 5) {
return NextResponse.json({ error: 'Image is too large' });
}

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

const response = await new Promise<CloudinaryResponse | undefined>(
(resolve, reject) => {
cloudinary.uploader
.upload_stream({}, (error, result) => {
if (error) {
reject(error);
return;
}
resolve(result);
})
.end(buffer);
},
);

if (!response) {
return NextResponse.json({ error: 'Image upload failed' });
}

const image = await createImageInsecure(response.secure_url);

if (!image) {
return NextResponse.json({ error: 'Image upload failed' });
}

return NextResponse.json({ imageUrl: image.url });
} catch (error) {
return NextResponse.json({
error: (error as Error).message,
});
}
}

// import { v2 as cloudinary } from 'cloudinary';
// import { NextRequest, NextResponse } from 'next/server';

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

// // eslint-disable-next-line no-restricted-syntax
// export async function POST(request: NextRequest): Promise<NextResponse> {
// const body = (await request.json()) as {
// paramsToSign: Record<string, string>;
// };

// const { paramsToSign } = body;

// const signature = cloudinary.utils.api_sign_request(
// paramsToSign,
// process.env.CLOUDINARY_API_SECRET as string,
// );

// return NextResponse.json({ signature });
// }
20 changes: 20 additions & 0 deletions app/imageUpload/EventImage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
'use client';
import { CldImage } from 'next-cloudinary';
import { SiteEvent } from '../../database/events';

type Props = {
event: SiteEvent;
};

export default function EventImage(props: Props) {
return (
<CldImage
width="150"
height="150"
src={props.event.image}
crop="fill"
sizes="100vw"
alt={`${props.event.name} event picture`}
/>
);
}
65 changes: 65 additions & 0 deletions app/imageUpload/ImageForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
'use client';

import { useRouter } from 'next/navigation';
import { FormEvent, useState } from 'react';
import { ImageUploadResponsePost } from '../api/imageUpload/route';
import ErrorMessage from '../ErrorMessage';
import { SubmitButton } from './SubmitButton';

interface ErrorResponse {
error: string;
}

export default function ImageForm({
buttonTitle,
formTitle,
}: {
buttonTitle: string;
formTitle: string;
}) {
const [errorMessage, setErrorMessage] = useState('');
const [successMessage, setSuccessMessage] = useState('');
const router = useRouter();

async function handleUpload(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
const formData = new FormData(event.currentTarget);

const response = await fetch('/api/imageUpload', {
method: 'POST',
body: formData,
});

if (!response.ok) {
const errorData: ErrorResponse = await response.json();
setErrorMessage(errorData.error);
return;
}

const data: ImageUploadResponsePost | ErrorResponse = await response.json();

if ('error' in data) {
setErrorMessage(data.error);
return;
}

router.refresh();

setSuccessMessage('Image uploaded successfully');
}

return (
<div>
{!!successMessage && <p>{successMessage}</p>}
<strong>{formTitle}</strong>
<form onSubmit={handleUpload}>
<label>
Select Image:
<input type="file" name="image" accept="image/*" />
</label>
<SubmitButton buttonTitle={buttonTitle} />
</form>
{!!errorMessage && <ErrorMessage>{errorMessage}</ErrorMessage>}
</div>
);
}
11 changes: 11 additions & 0 deletions app/imageUpload/SubmitButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
'use client';

import { useFormStatus } from 'react-dom';

export function SubmitButton({ buttonTitle }: { buttonTitle: string }) {
const { pending } = useFormStatus();

return (
<button disabled={pending}>{pending ? 'Loading...' : buttonTitle}</button>
);
}
59 changes: 59 additions & 0 deletions app/imageUpload/imageUpload.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
'use client';
import { CldUploadWidget } from 'next-cloudinary';
import React, { useState } from 'react';

interface UploadedAssetData {
public_id: string;
width: number;
height: number;
id: string;
}

export default function ImageUpload() {
const [pictureUrl, setPictureUrl] = useState('');
const [resultPicture, setResultPicture] = useState<UploadedAssetData>();

console.log(resultPicture);

return (
<div>
<div>
<span>Event Poster:</span>
{!!pictureUrl && (
<div>
<img src={pictureUrl} alt="Event Poster" />
</div>
)}
<CldUploadWidget
signatureEndpoint="/api/sign-image"
onSuccess={(res) => {
setResultPicture(res.info as UploadedAssetData);
try {
if (typeof res.info === 'string') {
throw new Error('Unexpected string in res.info');
}
if (typeof res.info === 'undefined') {
throw new Error('Unexpected undefined in res.info');
}
const secureUrl = res.info.secure_url;
setPictureUrl(secureUrl);
} catch (error) {
console.error('Error:', error);
}
}}
>
{({ open }) => {
return (
<button
onClick={() => open()}
className="input input-bordered w-full py-3 px-4 text-center"
>
Upload an image
</button>
);
}}
</CldUploadWidget>
</div>
</div>
);
}
34 changes: 34 additions & 0 deletions app/imageUpload/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import Image from 'next/image';
import { getImagesInsecure } from '../../database/imgQueries';
import ImageForm from './ImageForm';

export default async function imageUploadPage() {
const images = await getImagesInsecure();

return (
<div>
<div>
<h1>Upload Image to Cloudinary</h1>

{images.length > 0 && (
<div>
<h2>Images</h2>
<ul>
{images.map((image) => (
<li key={`image-${image.id}`}>
<Image
src={image.url}
alt="Uploaded image"
width={40}
height={40}
/>
</li>
))}
</ul>
</div>
)}
<ImageForm buttonTitle="Upload Image" formTitle="Upload Image" />
</div>
</div>
);
}
19 changes: 16 additions & 3 deletions app/post/PostEventForm.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use client';

import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { ChangeEvent, useState } from 'react';
import { PostEventsResponseBodyPost } from '../api/postedEvents/route';
Expand Down Expand Up @@ -29,6 +30,12 @@ export default function PostEventForm(props: Props) {

const router = useRouter();

function addImageUrl(url: string) {
setPostEvent({ ...postEvent, image: url });
}

console.log(addImageUrl);

async function handleCreate(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();

Expand Down Expand Up @@ -58,7 +65,7 @@ export default function PostEventForm(props: Props) {
return;
}

router.push('/events');
router.push('/');
}

function handleChange(
Expand All @@ -85,15 +92,21 @@ export default function PostEventForm(props: Props) {
<p>* stands for required fields</p>
</hgroup>
</div>
<br />
<form onSubmit={handleCreate}>
<div>
<label htmlFor="image">Upload Image: </label>
<div>
<h3>Upload Image</h3>
</div>
<Link href="/imageUpload">Poster for Event</Link>

{/* <label htmlFor="image">Upload Image: </label>
<input
id="image"
name="image"
value={postEvent.image}
onChange={handleChange}
/>
/> */}
</div>
<br />
<br />
Expand Down
4 changes: 2 additions & 2 deletions database/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ export const eventSchema = z.object({
entryFee: z.number(),
category: z.string(),
description: z.string(),
organizerUrl: z.string(),
image: z.string(),
organizerUrl: z.string().url(),
image: z.string().url(),
ageRestriction: z.boolean(),
archived: z.boolean(),
});
Expand Down
Loading

0 comments on commit 682071c

Please sign in to comment.