Skip to content

Commit

Permalink
refactor: code cleanup (#7)
Browse files Browse the repository at this point in the history
* refactor: code cleanup

* refactor: more cleanup

* chore: more documentation
  • Loading branch information
QuiiBz authored Dec 27, 2023
1 parent 4abca97 commit 29b56aa
Show file tree
Hide file tree
Showing 29 changed files with 1,206 additions and 859 deletions.
13 changes: 10 additions & 3 deletions apps/dashboard/src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
'use client'
import { OgPlayground } from "../components/OgPlayground";
import { OgEditor } from "../components/OgEditor";
import { createElementId } from "../lib/elements";
import type { OGElement } from "../lib/types";

/**
* The initial elements to render in the editor.
*
* It only contains a single element, a white background that
* takes the entire width and height of the editor.
*/
const initialElements: OGElement[] = [
{
id: Math.random().toString(),
id: createElementId(),
tag: 'div',
x: 0,
y: 0,
Expand All @@ -19,6 +26,6 @@ const initialElements: OGElement[] = [

export default function Home() {
return (
<OgPlayground height={630} initialElements={initialElements} width={1200} />
<OgEditor height={630} initialElements={initialElements} width={1200} />
)
}
5 changes: 3 additions & 2 deletions apps/dashboard/src/components/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ interface ButtonProps {
icon?: ReactNode
variant?: 'danger'
onClick: () => void
isLoading?: boolean
className?: string
children: ReactNode
}

export function Button({ icon, variant, onClick, className, children }: ButtonProps) {
export function Button({ icon, variant, onClick, isLoading, className, children }: ButtonProps) {
return (
<button className={`flex gap-3 items-center px-3 py-1 border rounded ${variant === 'danger' ? 'text-red-900 bg-red-50 border-red-200 hover:border-red-300' : 'text-gray-900 bg-gray-50 border-gray-200 hover:border-gray-300'} ${className}`} onClick={onClick} type="button">
<button className={`flex gap-3 items-center px-3 py-1 border rounded ${variant === 'danger' ? 'text-red-900 bg-red-50 border-red-200 hover:border-red-300' : 'text-gray-900 bg-gray-50 border-gray-200 hover:border-gray-300'} ${isLoading ? 'cursor-not-allowed opacity-60' : ''} ${className}`} onClick={isLoading ? undefined : onClick} type="button">
{icon}
{children}
</button>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,35 +1,33 @@
import type { ReactElement } from "react"
import type { OGElement } from "../lib/types"
import { createElement } from "../lib/elements"
import { TextIcon } from "./icons/TextIcon"
import { CircleIcon } from "./icons/CircleIcon"
import { ImageIcon } from "./icons/ImageIcon"
import { useOg } from "./OgPlayground"
import { useOg } from "./OgEditor"
import { BoxIcon } from "./icons/BoxIcon"
import { MagicWandIcon } from "./icons/MagicWandIcon"

interface ToolbarButtonProps {
element: OGElement
element: Partial<OGElement>
children: ReactElement
}

function ToolbarButton({ element, children }: ToolbarButtonProps) {
const { addElement } = useOg()

return (
<button className="p-2 text-gray-600 hover:text-gray-900" onClick={() => { addElement(element); }} type="button">
<button className="p-2 text-gray-600 hover:text-gray-900" onClick={() => { addElement(createElement(element)); }} type="button">
{children}
</button>
)
}

export function PlaygroundToolbar() {
export function EditorToolbar() {
return (
<div className="rounded-md border border-gray-100 bg-white z-10 flex flex-row items-center">
<ToolbarButton element={{
tag: 'p',
id: String(Math.random()),
x: (1200 - 100) / 2,
y: (630 - 50) / 2,
width: 100,
height: 50,
visible: true,
Expand All @@ -48,9 +46,6 @@ export function PlaygroundToolbar() {
<div className="w-[1px] h-4 bg-gray-100" />
<ToolbarButton element={{
tag: 'div',
id: String(Math.random()),
x: (1200 - 200) / 2,
y: (630 - 200) / 2,
width: 200,
height: 200,
visible: true,
Expand All @@ -63,9 +58,6 @@ export function PlaygroundToolbar() {
<div className="w-[1px] h-4 bg-gray-100" />
<ToolbarButton element={{
tag: 'div',
id: String(Math.random()),
x: (1200 - 150) / 2,
y: (630 - 150) / 2,
width: 150,
height: 150,
visible: true,
Expand All @@ -79,9 +71,6 @@ export function PlaygroundToolbar() {
<div className="w-[1px] h-4 bg-gray-100" />
<ToolbarButton element={{
tag: 'div',
id: String(Math.random()),
x: (1200 - 200) / 2,
y: (630 - 150) / 2,
width: 200,
height: 150,
visible: true,
Expand All @@ -96,9 +85,6 @@ export function PlaygroundToolbar() {
<div className="w-[1px] h-4 bg-gray-100" />
<ToolbarButton element={{
tag: 'span',
id: String(Math.random()),
x: (1200 - 312) / 2,
y: (630 - 50) / 2,
width: 312,
height: 50,
visible: true,
Expand Down
112 changes: 18 additions & 94 deletions apps/dashboard/src/components/Element.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,7 @@
import type { CSSProperties } from "react";
import { useEffect, useMemo, useRef, useState } from "react"
import type { OGElement } from "../lib/types";
import { useOg } from "./OgPlayground"

function hexToRgba(hex: string, alpha: number) {
const bigint = parseInt(hex.replace('#', ''), 16);
const r = (bigint >> 16) & 255;
const g = (bigint >> 8) & 255;
const b = bigint & 255;

return `rgba(${r}, ${g}, ${b}, ${alpha / 100})`;
}
import { createElementStyle } from "../lib/elements";
import { useOg } from "./OgEditor"

interface ElementProps {
element: OGElement
Expand All @@ -24,13 +15,6 @@ export function Element({ element }: ElementProps) {
const isSelected = selectedElement === element.id
const Tag = element.tag

useEffect(() => {
if (isEditing && !isSelected) {
elementRef.current?.blur()
}

}, [isEditing, isSelected])

useEffect(() => {
function onMouseDown(event: MouseEvent) {
if (isEditing) {
Expand Down Expand Up @@ -62,6 +46,7 @@ export function Element({ element }: ElementProps) {
function onMouseMove(mouseMoveEvent: MouseEvent) {
changed = true

// We want to resize / rotate
if (isResizer) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- we know it's not null
const parent = target.parentElement!
Expand Down Expand Up @@ -133,6 +118,7 @@ export function Element({ element }: ElementProps) {
parent.style.transform = `rotate(${rotate}deg)`
}
} else {
// We want to move
const x = mouseMoveEvent.clientX - startX
const y = mouseMoveEvent.clientY - startY

Expand All @@ -149,16 +135,7 @@ export function Element({ element }: ElementProps) {
return
}

if (!isResizer) {
const x = Number(target.style.left.replace('px', ''))
const y = Number(target.style.top.replace('px', ''))

updateElement({
...element,
x,
y,
})
} else {
if (isResizer) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- we know it's not null
const parent = target.parentElement!
const x = Number(parent.style.left.replace('px', ''))
Expand All @@ -175,6 +152,15 @@ export function Element({ element }: ElementProps) {
height,
rotate,
})
} else {
const x = Number(target.style.left.replace('px', ''))
const y = Number(target.style.top.replace('px', ''))

updateElement({
...element,
x,
y,
})
}
}

Expand All @@ -187,6 +173,7 @@ export function Element({ element }: ElementProps) {

const target = event.target as HTMLElement

// Prevent double-clicking on resizers
if (!target.className.includes('element')) {
return
}
Expand All @@ -196,6 +183,8 @@ export function Element({ element }: ElementProps) {
setIsEditing(true)

function onKeyDown(keyDownEvent: KeyboardEvent) {
// Submit on enter or escape. The actual cleanup is done in the
// onBlur event handler.
if (keyDownEvent.key === 'Enter' || keyDownEvent.key === 'Escape') {
keyDownEvent.preventDefault()
target.blur()
Expand Down Expand Up @@ -240,72 +229,7 @@ export function Element({ element }: ElementProps) {
}
}, [element.tag, elementRef, isEditing, setSelectedElement, updateElement, removeElement, selectedElement, isSelected, element])

const style = useMemo<CSSProperties>(() => {
const boxShadows: string[] = []
let textShadow: string | undefined

if (element.border) {
boxShadows.push(`0 0 0 ${element.border.width}px${element.border.style === 'inside' ? ' inset' : ''} ${element.border.color}`)
}

if (element.shadow) {
if (element.tag === 'p' || element.tag === 'span') {
textShadow = `${element.shadow.x}px ${element.shadow.y}px ${element.shadow.blur}px ${element.shadow.color}`
} else {
boxShadows.push(`${element.shadow.x}px ${element.shadow.y}px ${element.shadow.blur}px ${element.shadow.width}px ${element.shadow.color}`)
}
}

let base: CSSProperties = {
position: 'absolute',
top: `${element.y}px`,
left: `${element.x}px`,
width: `${element.width}px`,
height: `${element.height}px`,
transform: element.rotate !== 0 ? `rotate(${element.rotate}deg)` : undefined,
boxShadow: boxShadows.length ? boxShadows.join(', ') : undefined,
}

if (element.tag === 'p' || element.tag === 'span') {
base = {
...base,
color: hexToRgba(element.color, element.opacity),
fontFamily: element.fontFamily,
fontWeight: element.fontWeight,
fontSize: `${element.fontSize}px`,
lineHeight: element.lineHeight,
textAlign: element.align,
marginTop: 0,
marginBottom: 0,
textShadow,
}
}

if (element.tag === 'div') {
base = {
...base,
display: 'flex',
borderRadius: element.radius ? `${element.radius}px` : undefined,
background: element.backgroundImage
? undefined
: element.gradient
? element.gradient.type === 'radial'
? `radial-gradient(${element.gradient.start}, ${element.gradient.end})`
: `linear-gradient(${element.gradient.angle}deg, ${element.gradient.start}, ${element.gradient.end})`
: hexToRgba(element.backgroundColor, element.opacity),
backgroundImage: element.backgroundImage ? `url(${element.backgroundImage})` : undefined,
backgroundRepeat: element.backgroundImage ? 'no-repeat' : undefined, // TODO
// backgroundPosition: element.backgroundImage ? 'center' : undefined, // TODO
backgroundSize: element.backgroundImage
? element.backgroundSize === 'cover'
? 'auto 100%'
: '100% 100%'
: undefined,
}
}

return Object.fromEntries(Object.entries(base).filter(([, value]) => value !== undefined));
}, [element])
const style = useMemo(() => createElementStyle(element), [element])

if (!element.visible) {
return null
Expand Down
3 changes: 2 additions & 1 deletion apps/dashboard/src/components/ElementTab.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useSortable } from "@dnd-kit/sortable"
import { CSS } from '@dnd-kit/utilities';
import type { OGElement } from "../lib/types";
import { useOg } from "./OgPlayground"
import { useOg } from "./OgEditor"
import { BoxIcon } from "./icons/BoxIcon"
import { CircleIcon } from "./icons/CircleIcon"
import { ImageIcon } from "./icons/ImageIcon"
Expand Down Expand Up @@ -39,6 +39,7 @@ export function ElementTab({ element }: ElementTabProps) {
{element.tag === 'p' ? (
<>
<TextIcon height="1.4em" width="1.4em" />
{/* TODO: use ellipsis instead of slicing */}
{element.content.slice(0, 25)}
</>
) : null}
Expand Down
Loading

0 comments on commit 29b56aa

Please sign in to comment.