Skip to content

Commit

Permalink
final touches and polish
Browse files Browse the repository at this point in the history
  • Loading branch information
dray92 committed Nov 13, 2024
1 parent bfc2017 commit 35af5c9
Show file tree
Hide file tree
Showing 10 changed files with 414 additions and 30 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# DevZero @ KubeCon 2024

This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). Development of this project has greatly benefitted from [V0](v0.dev).

![Screenshot of the webapp](frame.png)

## Getting Started

Expand Down
Binary file added frame.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
140 changes: 140 additions & 0 deletions src/app/admin/kubecon-booth/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
'use client'

import { useState, useEffect } from 'react'
import Image from 'next/image'
import { Button } from "@/components/ui/button"

interface Submission {
_id: string
firstName: string
lastName: string
companyName: string
email: string
address: string
phone: string
originalImage: string
generatedImage: string
createdAt: string
}

export default function KubeConBoothPage() {
const [submissions, setSubmissions] = useState<Submission[]>([])
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | null>(null)

useEffect(() => {
fetchSubmissions()
}, [])

const fetchSubmissions = async () => {
try {
setIsLoading(true)
setError(null)
console.log('Fetching submissions...')
const response = await fetch('/api/admin/submissions')
if (!response.ok) {
throw new Error(`Failed to fetch submissions: ${response.statusText}`)
}
const data = await response.json()
console.log('Submissions fetched successfully:', data.length)
setSubmissions(data)
} catch (err) {
console.error('Error fetching submissions:', err)
setError('An error occurred while fetching submissions. Please try again.')
} finally {
setIsLoading(false)
}
}

const handleDownload = (base64Image: string, fileName: string) => {
const link = document.createElement('a')
link.href = `data:image/jpeg;base64,${base64Image}`
link.download = fileName
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}

if (isLoading) {
return <div className="text-center p-8">Loading submissions...</div>
}

if (error) {
return (
<div className="text-center p-8">
<p className="text-red-500 mb-4">{error}</p>
<Button onClick={fetchSubmissions}>Retry</Button>
</div>
)
}

if (submissions.length === 0) {
return <div className="text-center p-8">No submissions found.</div>
}

return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-2xl font-bold mb-6">KubeCon Booth Submissions</h1>
<div className="overflow-x-auto">
<table className="min-w-full bg-white">
<thead className="bg-gray-100">
<tr>
<th className="py-2 px-4 border-b text-left">Name</th>
<th className="py-2 px-4 border-b text-left">Company</th>
<th className="py-2 px-4 border-b text-left">Email</th>
<th className="py-2 px-4 border-b text-left">Address</th>
<th className="py-2 px-4 border-b text-left">Phone</th>
<th className="py-2 px-4 border-b text-left">Original Image</th>
<th className="py-2 px-4 border-b text-left">Generated Image</th>
<th className="py-2 px-4 border-b text-left">Created At</th>
</tr>
</thead>
<tbody>
{submissions.map((submission) => (
<tr key={submission._id}>
<td className="py-2 px-4 border-b">{`${submission.firstName} ${submission.lastName}`}</td>
<td className="py-2 px-4 border-b">{submission.companyName}</td>
<td className="py-2 px-4 border-b">{submission.email}</td>
<td className="py-2 px-4 border-b">{submission.address}</td>
<td className="py-2 px-4 border-b">{submission.phone}</td>
<td className="py-2 px-4 border-b">
<div className="relative w-20 h-20">
<Image
src={`data:image/jpeg;base64,${submission.originalImage}`}
alt="Original Image"
fill
className="object-cover"
/>
</div>
<Button
onClick={() => handleDownload(submission.originalImage, `original_${submission._id}.jpg`)}
className="mt-2 text-xs"
>
Download
</Button>
</td>
<td className="py-2 px-4 border-b">
<div className="relative w-20 h-20">
<Image
src={`data:image/jpeg;base64,${submission.generatedImage}`}
alt="Generated Image"
fill
className="object-cover"
/>
</div>
<Button
onClick={() => handleDownload(submission.generatedImage, `generated_${submission._id}.jpg`)}
className="mt-2 text-xs"
>
Download
</Button>
</td>
<td className="py-2 px-4 border-b">{new Date(submission.createdAt).toLocaleString()}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)
}
87 changes: 87 additions & 0 deletions src/app/api/admin/submissions/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { NextResponse } from 'next/server'
import { MongoClient } from 'mongodb'
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3'
import { Readable } from 'stream'

const s3Client = new S3Client({
region: process.env.AWS_REGION || 'us-east-1',
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID || '',
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || '',
},
})

const BUCKET_NAME = process.env.AWS_S3_BUCKET_NAME || 'kubecon-booth-1080b684c6ad44c9b835761fa92dece8'

async function streamToBuffer(stream: Readable): Promise<Buffer> {
const chunks: Buffer[] = []

for await (const chunk of stream) {
chunks.push(Buffer.from(chunk))
}

return Buffer.concat(chunks)
}

async function getImageFromS3(key: string): Promise<string> {
try {
console.log(`Fetching image from S3: ${key}`)
const command = new GetObjectCommand({
Bucket: BUCKET_NAME,
Key: key,
})

const response = await s3Client.send(command)
if (!response.Body) {
throw new Error('No image data received from S3')
}

const stream = response.Body as Readable
const buffer = await streamToBuffer(stream)
console.log(`Successfully fetched and processed image: ${key}`)
return buffer.toString('base64')
} catch (error) {
console.error(`Error fetching image from S3: ${key}`, error)
throw error
}
}

export async function GET() {
try {
console.log('Connecting to MongoDB...')
const client = await MongoClient.connect(process.env.MONGODB_URI || '')
const db = client.db('kubecon-booth')
console.log('Connected to MongoDB, fetching submissions...')
const submissions = await db.collection('submissions').find({}).toArray()
console.log(`Found ${submissions.length} submissions`)

const processedSubmissions = await Promise.all(submissions.map(async (submission) => {
try {
const originalImage = await getImageFromS3(submission.originalImagePath)
const generatedImage = await getImageFromS3(submission.generatedImagePath)

return {
...submission,
originalImage,
generatedImage,
}
} catch (error) {
console.error(`Error processing submission ${submission._id}:`, error)
return {
...submission,
originalImage: null,
generatedImage: null,
error: 'Failed to fetch images',
}
}
}))

await client.close()
console.log('Successfully processed all submissions')

return NextResponse.json(processedSubmissions)
} catch (error) {
console.error('Error fetching submissions:', error)
return NextResponse.json({ error: 'Internal Server Error', details: error }, { status: 500 })
}
}
1 change: 1 addition & 0 deletions src/app/api/submit-landing-form/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export async function POST(request: Request) {
to: email,
subject: 'Welcome to DevZero @ KubeCon 2024!',
replyTo: 'debo@devzero.io',
bcc: '21418179@bcc.hubspot.com',
html: emailHtml,
})

Expand Down
51 changes: 36 additions & 15 deletions src/app/api/submit-lego-sweepstakes/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ import { NextResponse } from 'next/server'
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'
import { MongoClient } from 'mongodb'
import { v4 as uuidv4 } from 'uuid'
import { Resend } from 'resend'
import { render } from '@react-email/render'
import LegoSubmitEmail from '@/emails/LegoSubmitEmail'

const resend = new Resend(process.env.RESEND_API_KEY)

const s3 = new S3Client({
region: process.env.AWS_REGION || 'us-east-1',
Expand All @@ -17,32 +22,26 @@ const BUCKET_NAME = 'kubecon-booth-1080b684c6ad44c9b835761fa92dece8'

export async function POST(request: Request) {
try {
const { formData, imageUrl, generatedImageUrl } = await request.json()
const { formData, originalImageB64, generatedImageId } = await request.json()

// Download both images
const [originalImage, generatedImage] = await Promise.all([
fetch(imageUrl).then(res => res.arrayBuffer()),
fetch(generatedImageUrl).then(res => res.arrayBuffer())
])
if (!originalImageB64 || !generatedImageId) {
throw new Error('Missing required image metadata')
}

// Generate unique IDs for the images
const originalImageId = uuidv4()
const generatedImageId = uuidv4()

// base64 decode the string to get the image buffer
const originalImageBuffer = Buffer.from(originalImageB64, 'base64')

// Upload both images to S3
await Promise.all([
s3.send(new PutObjectCommand({
Bucket: BUCKET_NAME,
Key: `original/${originalImageId}.jpg`,
Body: Buffer.from(originalImage),
Body: originalImageBuffer,
ContentType: 'image/jpeg',
})),
s3.send(new PutObjectCommand({
Bucket: BUCKET_NAME,
Key: `generated/${generatedImageId}.jpg`,
Body: Buffer.from(generatedImage),
ContentType: 'image/jpeg',
}))
])

// Connect to MongoDB
Expand All @@ -59,6 +58,28 @@ export async function POST(request: Request) {

await client.close()

const name = formData.firstName
// Render email template
console.log('Rendering email template fro lego')
const emailHtml = await render(LegoSubmitEmail({
name,
}))

console.log('Email HTML for lego rendered successfully')

// Send email with the rendered HTML
console.log('Attempting to send email')
const emailResult = await resend.emails.send({
from: 'DevZero <no-reply@devzero.io>',
to: formData.email,
subject: 'Your LEGO Minifigure from DevZero!',
replyTo: 'debo@devzero.io',
bcc: '21418179@bcc.hubspot.com',
html: emailHtml,
})

console.log('Email sent successfully:', emailResult)

return NextResponse.json({
success: true,
message: 'Submission successful'
Expand All @@ -68,7 +89,7 @@ export async function POST(request: Request) {
return NextResponse.json(
{
success: false,
error: 'Failed to process submission'
error: error instanceof Error ? error.message : 'Failed to process submission'
},
{ status: 500 }
)
Expand Down
24 changes: 12 additions & 12 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
'use client'

import { useEffect, useState } from 'react'
import { Inter } from 'next/font/google'
import './globals.css'
import { Footer } from '@/components/Footer'

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

export const metadata = {
title: 'KubeCon 2024 Booth Demo',
description: 'DevZero KubeCon 2024 Booth Demo',
}

export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
const [isMounted, setIsMounted] = useState(false)

useEffect(() => {
setIsMounted(true)
}, [])

return (
<html lang="en">
<body className={inter.className}>
{isMounted ? children : null}
<html lang="en" className="h-full">
<body className={`${inter.className} flex flex-col min-h-screen`}>
<main className="flex-grow">
{children}
</main>
<Footer />
</body>
</html>
)
Expand Down
Loading

0 comments on commit 35af5c9

Please sign in to comment.