Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/danielroe/roe.dev. This is the code and content for my personal website, built in Nuxt. roe.dev
Something went wrong. Try again.
10 kB · 290 lines
TSX
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291import React, { useState, useCallback, useRef, useEffect } from 'react'import { Stack, Text, Card, Button, Box, Flex } from '@sanity/ui'import { set, useFormValue, useClient } from 'sanity'import { domToBlob } from 'modern-screenshot'
interface ImageGeneratorProps { onChange: (event: any) => void renderDefault: (props: any) => React.ReactElement}
export function ImageGenerator (props: ImageGeneratorProps) { const { onChange, renderDefault } = props const [isGenerating, setIsGenerating] = useState(false) const [hasPreview, setHasPreview] = useState(false) const terminalRef = useRef<HTMLDivElement>(null)
const documentContent = useFormValue(['content']) as string const client = useClient({ apiVersion: '2025-02-10' })
useEffect(() => { if (documentContent?.trim()) { setHasPreview(true) } else { setHasPreview(false) } }, [documentContent])
const generateImage = useCallback(async () => { if (!documentContent || !hasPreview || !terminalRef.current) return
setIsGenerating(true)
try { const blob = await domToBlob(terminalRef.current, { width: 1200, height: 630, scale: 2, style: { transform: 'scale(1)', transformOrigin: 'top left', }, })
if (blob) { try { // Upload the image to Sanity as an asset const asset = await client.assets.upload('image', blob, { filename: `ama-generated-${Date.now()}.png`, })
onChange(set({ _type: 'image', asset: { _type: 'reference', _ref: asset._id, }, })) } catch (error) { console.error('Error uploading image to Sanity:', error) } } setIsGenerating(false) } catch (error) { console.error('Error generating image:', error) setIsGenerating(false) } }, [documentContent, hasPreview, onChange, client])
// Split text into lines that fit the terminal width const formatTextLines = useCallback((text: string) => { const words = text.split(' ') const lines: string[] = [] let currentLine = '' const length = 58
for (const word of words) { if (currentLine.length + word.length + 1 <= length) { currentLine += (currentLine ? ' ' : '') + word } else { if (currentLine) { lines.push(currentLine) } currentLine = word } } if (currentLine) { lines.push(currentLine) } return lines }, [])
const textLines = documentContent ? formatTextLines(documentContent) : []
return ( <Stack space={3}> <Card padding={3} tone="default" border> <Stack space={3}> <Flex justify="space-between" align="center"> <Text size={1} weight="semibold"> Generated Image </Text> <Button text={isGenerating ? 'Saving to Sanity...' : 'Save to Sanity'} tone="primary" onClick={generateImage} disabled={!hasPreview || isGenerating} loading={isGenerating} /> </Flex>
{documentContent && ( <Box> {/* Editor Mockup */} <div ref={terminalRef} style={{ width: '1200px', height: '630px', padding: '0px 96px', display: 'flex', flexDirection: 'column', justifyContent: 'center', boxSizing: 'border-box', transform: 'scale(0.47)', transformOrigin: 'top left', marginBottom: '-325px', position: 'relative', }} > {/* Editor Window */} <div style={{ width: '100%', background: '#ffffff', borderRadius: '16px', boxShadow: 'rgb(4, 4, 4) 0px 0px 0px 1px, rgba(255, 255, 255, 0.18) 0px 1px 0px inset, rgba(0, 0, 0, 0.6) 0px 0px 18px 1px', overflow: 'hidden', }} > {/* Editor Title Bar */} <div style={{ height: '64px', background: '#292d3e', borderTopLeftRadius: '16px', borderTopRightRadius: '16px', display: 'flex', alignItems: 'center', paddingLeft: '20px', position: 'relative', }} > {/* Traffic Light Buttons */} <div style={{ display: 'flex', gap: '14px' }}> <div style={{ width: '20px', height: '20px', borderRadius: '50%', background: '#ff5f57', border: '1px solid #e53e3e', }} /> <div style={{ width: '20px', height: '20px', borderRadius: '50%', background: '#ffbd2e', border: '1px solid #d69e2e', }} /> <div style={{ width: '20px', height: '20px', borderRadius: '50%', background: '#28ca42', border: '1px solid #38a169', }} /> </div>
{/* Editor Title */} <div style={{ position: 'absolute', left: '50%', transform: 'translateX(-50%)', color: '#64748b', fontSize: '22px', fontWeight: '500', fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', }} > ~/answer.md </div> </div>
{/* Editor Content */} <div style={{ padding: '40px', display: 'flex', flexDirection: 'column', fontWeight: 600, fontFamily: 'JetBrains Mono', fontSize: '24px', lineHeight: '32px', paddingTop: '48px', paddingBottom: '64px', color: '#d0d0d0', background: '#292d3e', }} > {/* Content Lines */} <div style={{ flex: 1 }}> {textLines.map((line, index) => ( <div key={index} style={{ display: 'flex', alignItems: 'flex-start', marginBottom: '4px' }}> {/* Line Number */} <div style={{ width: '36px', textAlign: 'right', color: '#94a3b8', paddingRight: '32px', flexShrink: 0, userSelect: 'none', fontSize: '16px', }} > {(index + 1).toString().padStart(2, ' ')} </div>
{/* Content */} <div> {line} </div> </div> ))} </div> </div> </div> {/* Bottom Branding */} <div style={{ color: '#ffffff', height: 0, fontFamily: 'JetBrains Mono', fontSize: '24px', marginRight: '1rem', textAlign: 'right', fontWeight: '500', }} > <div style={{ marginTop: '1.25rem' }}> roe.dev/ama </div> </div> <div style={{ position: 'absolute', inset: '0', overflow: 'hidden', zIndex: -1, }} > <div ref={terminalRef} style={{ position: 'absolute', background: 'linear-gradient(yellow 5%, fuchsia, royalblue 95%)', filter: 'url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMCI+IDxmaWx0ZXIgaWQ9Im15RmlsdGVyIj4gPGZlVHVyYnVsZW5jZSB0eXBlPSJmcmFjdGFsTm9pc2UiIGJhc2VGcmVxdWVuY3k9Ii4wMDUgLjAwMSIgbnVtT2N0YXZlcz0iMiIgLz4gPGZlRGlzcGxhY2VtZW50TWFwIHhDaGFubmVsU2VsZWN0b3I9IlIiIHNjYWxlPSI1MDAiIGluPSJTb3VyY2VHcmFwaGljIiByZXN1bHQ9ImJhbmRzIiAvPiA8ZmVUdXJidWxlbmNlIHR5cGU9ImZyYWN0YWxOb2lzZSIgYmFzZUZyZXF1ZW5jeT0iMy43MSIgLz4gPGZlRGlzcGxhY2VtZW50TWFwIGluPSJiYW5kcyIgc2NhbGU9IjMyIiB4Q2hhbm5lbFNlbGVjdG9yPSJSIiAvPiA8L2ZpbHRlcj4gPC9zdmc+#myFilter")', inset: '-9em', }} /> </div> </div> </Box> )} </Stack> </Card>
{renderDefault(props)} </Stack> )}