Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added bun.lockb
Binary file not shown.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"@radix-ui/react-slot": "^1.1.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"html2canvas": "^1.4.1",
"lucide-react": "^0.468.0",
"next": "15.1.0",
"react": "^19.0.0",
Expand Down
65 changes: 59 additions & 6 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { PhotoUploader } from '@/components/photo-uploader'
import { VoiceRecorder } from '@/components/voice-recorder'
import { DoodleDrawer } from '@/components/doodle-drawer'
import { DottedBackground } from '@/components/dotted-background'
import html2canvas from 'html2canvas'
import { Download } from 'lucide-react'
// import { SpotifyPlayer } from '@/components/spotify-player'

export interface LetterItem {
Expand Down Expand Up @@ -62,7 +64,7 @@ export default function DigitalLetterComposer() {
const rect = (e.target as HTMLElement).getBoundingClientRect()
const offsetX = position.clientX - rect.left
const offsetY = position.clientY - rect.top

setIsDragging(true)
setCurrentItem({
...item,
Expand All @@ -77,7 +79,7 @@ export default function DigitalLetterComposer() {

const handleDragMove = (e: React.MouseEvent | React.TouchEvent) => {
if (!isDragging || !currentItem || !canvasRef.current) return

const position = 'touches' in e ? e.touches[0] : e
const rect = canvasRef.current.getBoundingClientRect()
const x = position.clientX - rect.left - (currentItem?.offsetX ?? 0)
Expand Down Expand Up @@ -122,11 +124,55 @@ export default function DigitalLetterComposer() {
})
}

const handleSendGift = async () => {
// In a real application, you would send the gift data to your backend here
// and generate a unique sharable link. For this example, we'll simulate it.
await new Promise(resolve => setTimeout(resolve, 1500)) // Simulate network request
const uniqueId = Math.random().toString(36).substring(2, 15)
return `https://yourdomain.com/gift/${uniqueId}`
}

const exportAsImage = async () => {
if (canvasRef.current) {
// Get the export button element
const exportButton = document.querySelector('.export-button')
if (exportButton) {
// Hide the button
exportButton.classList.add('hidden')
}

const canvas = await html2canvas(canvasRef.current, {
allowTaint: true,
useCORS: true,
foreignObjectRendering: true,
onclone: (clonedDoc) => {
// Force all SVGs to be rendered before capturing
const svgs = clonedDoc.getElementsByTagName('svg')
Array.from(svgs).forEach(svg => {
svg.setAttribute('width', svg.getBoundingClientRect().width.toString())
svg.setAttribute('height', svg.getBoundingClientRect().height.toString())
})
}
})

// Show the button again
if (exportButton) {
exportButton.classList.remove('hidden')
}

const image = canvas.toDataURL('image/png', 1.0)
const link = document.createElement('a')
link.href = image
link.download = `digital-letter-${Date.now()}.png`
link.click()
}
}

return (
<DndProvider backend={HTML5Backend}>
<div className="h-screen overflow-hidden bg-stone-200 flex flex-col relative">
<DottedBackground />
<main
<main
className="flex-1 relative overflow-hidden z-20"
ref={canvasRef}
onMouseMove={handleDragMove}
Expand All @@ -135,15 +181,23 @@ export default function DigitalLetterComposer() {
onTouchEnd={handleDragEnd}
onMouseLeave={handleDragEnd}
>
<LetterCanvas
items={items}
<LetterCanvas
items={items}
updateItemPosition={updateItemPosition}
updateItemContent={updateItemContent}
deleteItem={deleteItem}
handleDragStart={handleDragStart}
isDragging={isDragging}
currentItem={currentItem}
/>
<div className="absolute top-4 right-4 flex gap-2 z-10">
<button
onClick={exportAsImage}
className="export-button bg-stone-500 text-[#262626] hover:bg-stone-400 text-white p-2 rounded-lg shadow transition-colors font-bold"
>
<Download />
</button>
</div>
</main>
<div className="absolute bottom-0 left-0 right-0 z-30">
<Toolbar
Expand Down Expand Up @@ -198,4 +252,3 @@ export default function DigitalLetterComposer() {
</DndProvider>
)
}

48 changes: 30 additions & 18 deletions src/components/doodle-drawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,23 +91,36 @@ export const DoodleDrawer: React.FC<DoodleDrawerProps> = ({ onClose, onDoodleAdd
const svg = svgRef.current
if (!svg) return

// Clone the SVG to remove event listeners and refs
const clonedSvg = svg.cloneNode(true) as SVGSVGElement

// Calculate the viewBox based on the SVG's dimensions
const rect = svg.getBoundingClientRect()
clonedSvg.setAttribute('viewBox', `0 0 ${rect.width} ${rect.height}`)
clonedSvg.setAttribute('width', '100%')
clonedSvg.setAttribute('height', '100%')
clonedSvg.style.backgroundColor = 'transparent'

// Convert to string
const svgData = new XMLSerializer().serializeToString(clonedSvg)
const svgBlob = new Blob([svgData], { type: 'image/svg+xml' })
const svgUrl = URL.createObjectURL(svgBlob)

onDoodleAdd(svgUrl)
onClose()
// Create a new SVG with the paths
const svgContent = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${svg.clientWidth} ${svg.clientHeight}">
${paths.map(path => `<path d="${path}" stroke="${color}" stroke-width="${lineWidth}" fill="none" />`).join('')}
</svg>
`

// Convert SVG to base64 data URL
const blob = new Blob([svgContent], { type: 'image/svg+xml' })
const reader = new FileReader()
reader.onload = () => {
if (typeof reader.result === 'string') {
// Convert SVG to PNG using canvas
const img = new Image()
img.onload = () => {
const canvas = document.createElement('canvas')
canvas.width = svg.clientWidth
canvas.height = svg.clientHeight
const ctx = canvas.getContext('2d')
if (ctx) {
ctx.drawImage(img, 0, 0)
const pngUrl = canvas.toDataURL('image/png')
onDoodleAdd(pngUrl)
onClose()
}
}
img.src = reader.result
}
}
reader.readAsDataURL(blob)
}

return (
Expand Down Expand Up @@ -206,4 +219,3 @@ export const DoodleDrawer: React.FC<DoodleDrawerProps> = ({ onClose, onDoodleAdd
</Dialog>
)
}

7 changes: 3 additions & 4 deletions src/components/draggable-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,9 @@ export const DraggableItem: React.FC<DraggableItemProps> = ({
return <SpotifyPlayer spotifyUrl={item.content as string} />
case 'doodle':
return (
<object
data={item.content as string}
type="image/svg+xml"
<img
src={item.content as string}
alt="Doodle"
className="w-48 h-48 pointer-events-none"
/>
)
Expand Down Expand Up @@ -108,4 +108,3 @@ export const DraggableItem: React.FC<DraggableItemProps> = ({
</div>
)
}