Skip to content

Commit

Permalink
Merge pull request #1 from nousantx/refactor-app
Browse files Browse the repository at this point in the history
Add better performance and layout with preact
  • Loading branch information
nousantx authored Nov 17, 2024
2 parents c134fb8 + a6284f0 commit 730431a
Show file tree
Hide file tree
Showing 27 changed files with 1,948 additions and 1 deletion.
24 changes: 24 additions & 0 deletions app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
20 changes: 20 additions & 0 deletions app/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Preact</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:ital,wght@0,100..900;1,100..900&family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap"
rel="stylesheet"
id="google-fonts"
/>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
25 changes: 25 additions & 0 deletions app/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "app",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"preact": "^10.24.3"
},
"devDependencies": {
"@nousantx/color-generator": "^1.4.1",
"@nousantx/list-attribute": "^0.1.0",
"@nousantx/someutils": "^0.3.1",
"@preact/preset-vite": "^2.9.1",
"@remixicon/react": "^4.5.0",
"@tenoxui/core": "^1.3.0-alpha.3",
"@tenoxui/property": "^1.4.5",
"lucide-react": "^0.460.0",
"vite": "^5.4.10"
}
}
1 change: 1 addition & 0 deletions app/public/vite.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
107 changes: 107 additions & 0 deletions app/src/app.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { useState, useRef, useEffect } from 'preact/hooks'
import Preview from './components/livePreview.jsx'
import Controls from './components/controls.jsx'
import ImagePreview from './components/imagePreview.jsx'
import { useImageGeneration } from './hooks/useImageGeneration.js'
import { useDesignManagement } from './hooks/useDesignManagement.js'
import { init } from './styles/init'
import { RiHtml5Line } from '@remixicon/react'

export function App() {
init()
const [htmlContent, setHtmlContent] = useState(
`<div class="box-1000px center bg-teal-500">
<div class="relative bg-blue-600 text-neutral-100 center br-1rem family-sans box-250px fs-2rem fw-500 ls--0.015em shadow-lg">
Hello World!
<div class="absolute px-1rem py-6px bg-neutral-50 br-8px fs-1.5rem text-neutral-950 b-1rem r--6rem shadow-md">Might be a great day!</div>
</div>
</div>`
)
const [error, setError] = useState('')
const [scale, setScale] = useState(1)
const [outputFormat, setOutputFormat] = useState('png')
const [width, setWidth] = useState(1000)
const [height, setHeight] = useState(1000)

const canvasRef = useRef(null)
const previewRef = useRef(null)
const fileInputRef = useRef(null)

const { generateImage, downloadImage } = useImageGeneration(
canvasRef,
htmlContent,
width,
height,
scale,
outputFormat,
setError
)

const { saveDesign, loadDesign } = useDesignManagement(
htmlContent,
width,
height,
scale,
outputFormat,
setHtmlContent,
setWidth,
setHeight,
setScale,
setOutputFormat,
setError,
generateImage
)

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

return (
<main className="p-2rem">
<Preview ref={previewRef} htmlContent={htmlContent} />

<section
class="mt-2rem bg-neutral-50 text-neutral-400 br-1rem family-code shadow-xl shadow-neutral-950 bw-2px bs-solid bdr-c-neutral-100"
child="(textarea.main-input): w-100% h-mn-400px p-2rem over-x-scroll tw-nowrap bdr-none text-orange-400 bgc-transparent focus:[bdr,outline]-none;"
>
<div class="w-full p-1.5rem center relative bw-0 bw-bottom-2px bs-solid bdr-c-neutral-100">
<div class="center gap-8px d-none">
<RiHtml5Line size="18" />
</div>
<span class="family-code fs-14px">index.html</span>
<div class="center gap-8px absolute l-2rem" child="(div): box-16px br-100%;">
<div class="bg-green-500"></div>
<div class="bg-yellow-500"></div>
<div class="bg-red-500"></div>
</div>
</div>
<textarea
value={htmlContent}
onChange={(e) => setHtmlContent(e.target.value)}
className="main-input"
placeholder="Enter HTML here..."
/>
</section>

<Controls
scale={scale}
setScale={setScale}
width={width}
setWidth={setWidth}
height={height}
setHeight={setHeight}
outputFormat={outputFormat}
setOutputFormat={setOutputFormat}
error={error}
generateImage={generateImage}
downloadImage={downloadImage}
saveDesign={saveDesign}
loadDesign={loadDesign}
fileInputRef={fileInputRef}
setHtmlContent={setHtmlContent}
/>

<ImagePreview canvasRef={canvasRef} width={width} height={height} />
</main>
)
}
1 change: 1 addition & 0 deletions app/src/assets/preact.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
211 changes: 211 additions & 0 deletions app/src/components/controls.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
import { useState, useEffect } from 'react'
import { templates } from './design'

// Custom hook for debounced value
const useDebounce = (value, delay) => {
const [debouncedValue, setDebouncedValue] = useState(value)

useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value)
}, delay)

return () => {
clearTimeout(timer)
}
}, [value, delay])

return debouncedValue
}

export default function Controls({
scale,
setScale,
width,
setWidth,
height,
setHeight,
outputFormat,
setOutputFormat,
error,
generateImage,
downloadImage,
saveDesign,
loadDesign,
fileInputRef,
setHtmlContent
}) {
const [showTemplates, setShowTemplates] = useState(false)

// Local state for immediate input values
const [localWidth, setLocalWidth] = useState(width)
const [localHeight, setLocalHeight] = useState(height)
const [localScale, setLocalScale] = useState(scale)

// Debounced values
const debouncedWidth = useDebounce(localWidth, 300)
const debouncedHeight = useDebounce(localHeight, 300)
const debouncedScale = useDebounce(localScale, 300)

// Effect hooks to update parent state when debounced values change
useEffect(() => {
if (debouncedWidth >= 100 && debouncedWidth <= 4000) {
setWidth(debouncedWidth)
}
}, [debouncedWidth, setWidth])

useEffect(() => {
if (debouncedHeight >= 100 && debouncedHeight <= 4000) {
setHeight(debouncedHeight)
}
}, [debouncedHeight, setHeight])

useEffect(() => {
if (debouncedScale >= 0.1 && debouncedScale <= 5) {
setScale(debouncedScale)
}
}, [debouncedScale, setScale])

// Helper functions to safely parse input values
const handleWidthChange = (e) => {
const value = e.target.value
if (value === '') {
setLocalWidth(100) // Set to minimum value when empty
} else {
const parsed = parseInt(value)
if (!isNaN(parsed)) {
setLocalWidth(parsed)
}
}
}

const handleHeightChange = (e) => {
const value = e.target.value
if (value === '') {
setLocalHeight(100) // Set to minimum value when empty
} else {
const parsed = parseInt(value)
if (!isNaN(parsed)) {
setLocalHeight(parsed)
}
}
}

const handleScaleChange = (e) => {
const value = e.target.value
if (value === '') {
setLocalScale(0.1) // Set to minimum value when empty
} else {
const parsed = parseFloat(value)
if (!isNaN(parsed)) {
setLocalScale(parsed)
}
}
}

const handleTemplateClick = (template) => {
setHtmlContent(templates[template])
}

return (
<div>
<div className="mt-3rem br-8px">
<h3 className="mb-1.5rem fw-600 ls--0.015em lh-1">Try Templates</h3>
<div className="d-flex flex-w-wrap gap-8px">
{Object.keys(templates).map((template) => (
<button key={template} onClick={() => handleTemplateClick(template)} className="btn">
{template.charAt(0).toUpperCase() + template.slice(1)}
</button>
))}
</div>
</div>

<div className="mt-3rem">
<h3 className="mb-1.5rem fw-600 ls--0.015em lh-1">Image Tools</h3>
<div
className="flex flex-w-wrap gap-8px"
child="
(input,select): [all]-unset border bg-neutral-50 py-4px px-8px br-4px lh-1 h-25px w-min-content center iflex ml-8px bdr-c-neutral-300;
(input): w-60px;
(option): w-4px;
"
>
<label>
Width:
<input
type="number"
min="100"
max="4000"
value={localWidth}
onChange={handleWidthChange}
onBlur={() => {
if (localWidth < 100) setLocalWidth(100)
if (localWidth > 4000) setLocalWidth(4000)
}}
/>
</label>
<label>
Height:
<input
type="number"
min="100"
max="4000"
value={localHeight}
onChange={handleHeightChange}
onBlur={() => {
if (localHeight < 100) setLocalHeight(100)
if (localHeight > 4000) setLocalHeight(4000)
}}
/>
</label>
<label>
Scale:
<input
type="number"
min="0.1"
max="5"
step="0.1"
value={localScale}
onChange={handleScaleChange}
onBlur={() => {
if (localScale < 0.1) setLocalScale(0.1)
if (localScale > 5) setLocalScale(5)
}}
/>
</label>
<label>
Format:
<select value={outputFormat} onChange={(e) => setOutputFormat(e.target.value)}>
<option value="png">PNG</option>
<option value="jpeg">JPEG</option>
<option value="webp">WebP</option>
<option value="svg">SVG</option>
</select>
</label>
</div>
{error && <div className="text-red-500">{error}</div>}
</div>
<div className="d-flex flex-w-wrap mt-1rem gap-8px">
<button onClick={generateImage} className="btn">
Generate Preview Image
</button>
<button onClick={downloadImage} className="btn">
Download Image
</button>
<button onClick={saveDesign} className="btn">
Save Design
</button>
<label className="btn">
Load Design
<input
type="file"
ref={fileInputRef}
accept=".json"
onChange={loadDesign}
className="d-none"
/>
</label>
</div>
</div>
)
}
Loading

0 comments on commit 730431a

Please sign in to comment.