-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1 from nousantx/refactor-app
Add better performance and layout with preact
- Loading branch information
Showing
27 changed files
with
1,948 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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? |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
) | ||
} |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
) | ||
} |
Oops, something went wrong.