Skip to content

Commit

Permalink
refactor: use zustand instead of react context (#27)
Browse files Browse the repository at this point in the history
* feat: create zoom store instead of context

* feat: add images store

* refactor: use shared layout

* refactor: create elements store instead of context

* feat: add undo/redo with zundo

* refactor: export & show image/template name
  • Loading branch information
QuiiBz authored Dec 30, 2023
1 parent 76b2725 commit f5debba
Show file tree
Hide file tree
Showing 35 changed files with 644 additions and 603 deletions.
4 changes: 3 additions & 1 deletion apps/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@
"react-dom": "^18",
"react-promise-suspense": "^0.3.4",
"satori": "^0.10.11",
"sonner": "^1.3.1"
"sonner": "^1.3.1",
"zundo": "^2.0.3",
"zustand": "^4.4.7"
},
"devDependencies": {
"@next/eslint-plugin-next": "^14.0.3",
Expand Down
10 changes: 9 additions & 1 deletion apps/dashboard/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import { Toaster } from 'sonner'
import { Suspense } from 'react'
import { Splash } from '../components/Splash'
import './globals.css'

const inter = Inter({ subsets: ['latin'] })
Expand All @@ -18,7 +20,13 @@ export default function RootLayout({
return (
<html lang="en">
<body className={inter.className}>
{children}
{/* OgSplash uses `useSearchParams()` so we need to wrap it in a Suspense to allow to statically render the page
https://nextjs.org/docs/app/building-your-application/rendering/server-components#dynamic-functions */}
<Suspense>
<Splash>
{children}
</Splash>
</Suspense>
<Toaster closeButton richColors />
</body>
</html>
Expand Down
11 changes: 2 additions & 9 deletions apps/dashboard/src/app/my-images/page.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,10 @@
import { Suspense } from "react";
import { OgSplash } from "../../components/OgSplash";
import { MyImagesSplash } from "../../components/Splash/MyImagesSplash";

export const metadata = {
title: 'My images - OG Studio',
description: 'My Open Graph images.',
}

export default function MyImages() {
return (
// OgSplash uses `useSearchParams()` so we need to wrap it in a Suspense
// to allow to statically render the page: https://nextjs.org/docs/app/building-your-application/rendering/server-components#dynamic-functions
<Suspense>
<OgSplash route="my-images" />
</Suspense>
)
return <MyImagesSplash />
}
11 changes: 2 additions & 9 deletions apps/dashboard/src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,10 @@
import { Suspense } from "react";
import { OgSplash } from "../components/OgSplash";
import { HomeSplash } from "../components/Splash/HomeSplash";

export const metadata = {
title: 'OG Studio',
description: 'Figma-like OG (Open Graph) Image builder.',
}

export default function Home() {
return (
// OgSplash uses `useSearchParams()` so we need to wrap it in a Suspense
// to allow to statically render the page: https://nextjs.org/docs/app/building-your-application/rendering/server-components#dynamic-functions
<Suspense>
<OgSplash route="splash" />
</Suspense>
)
return <HomeSplash />
}
11 changes: 2 additions & 9 deletions apps/dashboard/src/app/templates/page.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,10 @@
import { Suspense } from "react";
import { OgSplash } from "../../components/OgSplash";
import { TemplatesSplash } from "../../components/Splash/TemplatesSplash";

export const metadata = {
title: 'Templates - OG Studio',
description: 'Pre-made Open Graph image templates.',
}

export default function Templates() {
return (
// OgSplash uses `useSearchParams()` so we need to wrap it in a Suspense
// to allow to statically render the page: https://nextjs.org/docs/app/building-your-application/rendering/server-components#dynamic-functions
<Suspense>
<OgSplash route="templates" />
</Suspense>
)
return <TemplatesSplash />
}
18 changes: 4 additions & 14 deletions apps/dashboard/src/components/EditorToolbar.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import type { ReactElement } from "react"
import { createElement } from "../lib/elements"
import { useZoomStore } from "../stores/zoomStore"
import { useElementsStore } from "../stores/elementsStore"
import { TextIcon } from "./icons/TextIcon"
import { CircleIcon } from "./icons/CircleIcon"
import { ImageIcon } from "./icons/ImageIcon"
import { BoxIcon } from "./icons/BoxIcon"
import { MagicWandIcon } from "./icons/MagicWandIcon"
import { ZoomOutIcon } from "./icons/ZoomOutIcon"
import { ZoomInIcon } from "./icons/ZoomInIcon"
import { useOg } from "./OgEditor"

interface ToolbarButtonProps {
onClick: () => void
Expand All @@ -23,19 +24,8 @@ function ToolbarButton({ onClick, children }: ToolbarButtonProps) {
}

export function EditorToolbar() {
const { addElement, zoom, setZoom } = useOg()

function zoomIn() {
if (zoom >= 100) return

setZoom(zoom + 10)
}

function zoomOut() {
if (zoom <= 10) return

setZoom(zoom - 10)
}
const addElement = useElementsStore(state => state.addElement)
const { zoom, zoomIn, zoomOut } = useZoomStore()

return (
<div className="flex flex-row items-center z-10 gap-4">
Expand Down
13 changes: 8 additions & 5 deletions apps/dashboard/src/components/Element.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useEffect, useMemo, useRef, useState } from "react"
import type { OGElement } from "../lib/types";
import { createElementStyle } from "../lib/elements";
import { useOg } from "./OgEditor"
import { useElementsStore } from "../stores/elementsStore";

interface ElementProps {
element: OGElement
Expand All @@ -10,9 +10,12 @@ interface ElementProps {
export function Element({ element }: ElementProps) {
const elementRef = useRef<HTMLElement>(null)
const [isEditing, setIsEditing] = useState(false)
const { selectedElement, setSelectedElement, updateElement, removeElement, zoom } = useOg()
const selectedElementId = useElementsStore(state => state.selectedElementId)
const setSelectedElementId = useElementsStore(state => state.setSelectedElementId)
const updateElement = useElementsStore(state => state.updateElement)
const removeElement = useElementsStore(state => state.removeElement)

const isSelected = selectedElement === element.id
const isSelected = selectedElementId === element.id
const Tag = element.tag

useEffect(() => {
Expand All @@ -23,7 +26,7 @@ export function Element({ element }: ElementProps) {

event.preventDefault();

setSelectedElement(element.id)
setSelectedElementId(element.id)

const target = event.target as HTMLElement
const isResizer = target.parentElement?.classList.contains('element')
Expand Down Expand Up @@ -227,7 +230,7 @@ export function Element({ element }: ElementProps) {
elementRef.current.removeEventListener('dblclick', onDoubleClick)
}
}
}, [element.tag, elementRef, isEditing, setSelectedElement, updateElement, removeElement, selectedElement, isSelected, element, zoom])
}, [element.tag, elementRef, isEditing, setSelectedElementId, updateElement, removeElement, selectedElementId, isSelected, element])

const style = useMemo(() => createElementStyle(element), [element])

Expand Down
85 changes: 0 additions & 85 deletions apps/dashboard/src/components/ElementTab.tsx

This file was deleted.

10 changes: 6 additions & 4 deletions apps/dashboard/src/components/LeftPanel/ElementRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,20 @@ import type { OGElement } from "../../lib/types";
import { NotVisibleIcon } from "../icons/NotVisibleIcon"
import { TextIcon } from "../icons/TextIcon"
import { VisibleIcon } from "../icons/VisibleIcon"
import { useOg } from "../OgEditor"
import { BoxIcon } from "../icons/BoxIcon"
import { CircleIcon } from "../icons/CircleIcon"
import { ImageIcon } from "../icons/ImageIcon"
import { MagicWandIcon } from "../icons/MagicWandIcon"
import { useElementsStore } from "../../stores/elementsStore";

interface ElementRowProps {
element: OGElement
}

export function ElementRow({ element }: ElementRowProps) {
const { selectedElement, setSelectedElement, updateElement } = useOg()
const selectedElementId = useElementsStore(state => state.selectedElementId)
const setSelectedElementId = useElementsStore(state => state.setSelectedElementId)
const updateElement = useElementsStore(state => state.updateElement)
const {
attributes,
listeners,
Expand All @@ -32,8 +34,8 @@ export function ElementRow({ element }: ElementRowProps) {
return (
<div className="flex justify-between items-center pb-2 cursor-auto" ref={setNodeRef} style={style} {...attributes} {...listeners}>
<button
className={`flex gap-2 select-none text-gray-600 hover:text-gray-900 w-full ${selectedElement === element.id ? '!text-blue-500' : ''} ${!element.visible ? '!text-gray-300' : ''}`}
onClick={() => { setSelectedElement(element.id); }}
className={`flex gap-2 select-none text-gray-600 hover:text-gray-900 w-full ${selectedElementId === element.id ? '!text-blue-500' : ''} ${!element.visible ? '!text-gray-300' : ''}`}
onClick={() => { setSelectedElementId(element.id); }}
type="button"
>
{element.tag === 'p'
Expand Down
6 changes: 4 additions & 2 deletions apps/dashboard/src/components/LeftPanel/ElementsSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import type { DragEndEvent } from "@dnd-kit/core";
import { DndContext, KeyboardSensor, PointerSensor, closestCenter, useSensor, useSensors } from "@dnd-kit/core";
import { SortableContext, arrayMove, sortableKeyboardCoordinates, verticalListSortingStrategy } from "@dnd-kit/sortable";
import { restrictToVerticalAxis } from "@dnd-kit/modifiers";
import { useOg } from "../OgEditor";
import { useElementsStore } from "../../stores/elementsStore";
import { ElementRow } from "./ElementRow";

export function ElementsSection() {
const { elements, setElements } = useOg()
const elements = useElementsStore(state => state.elements)
const setElements = useElementsStore(state => state.setElements)

const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
Expand Down
7 changes: 5 additions & 2 deletions apps/dashboard/src/components/LeftPanel/ExportSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,17 @@ import { useOg } from "../OgEditor";
import { domToReactElements, exportToPng, exportToSvg } from "../../lib/export";
import type { FontData } from "../../lib/fonts";
import { loadFonts } from "../../lib/fonts";
import { useElementsStore } from "../../stores/elementsStore";

export function ExportSection() {
const { rootRef, elements, setSelectedElement } = useOg()
const { rootRef } = useOg()
const elements = useElementsStore(state => state.elements)
const setSelectedElementId = useElementsStore(state => state.setSelectedElementId)
const [isLoading, setIsLoading] = useState(false)

async function exportSvg(showProgress = true) {
flushSync(() => {
setSelectedElement(null)
setSelectedElementId(null)
})

async function run() {
Expand Down
17 changes: 13 additions & 4 deletions apps/dashboard/src/components/LeftPanel/ModificationsSection.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,27 @@
import { useElementsStore } from "../../stores/elementsStore";
import { Button } from "../forms/Button";
import { useOg } from "../OgEditor";
import { DeleteIcon } from "../icons/DeleteIcon";
import { RedoIcon } from "../icons/RedoIcon";
import { UndoIcon } from "../icons/UndoIcon";

export function ModificationSection() {
const { undoRedo, reset } = useOg()
const { undo, redo, pastStates } = useElementsStore.temporal.getState()
const setElements = useElementsStore(state => state.setElements)

function reset() {
const initialState = pastStates.length >= 1 ? pastStates[0] : undefined

if (initialState?.elements) {
setElements(initialState.elements)
}
}

return (
<>
<p className="text-xs text-gray-600">Modifications</p>
<div className="grid grid-cols-2 gap-2 w-full">
<Button icon={<UndoIcon />} onClick={() => { undoRedo('undo'); }}>Undo</Button>
<Button icon={<RedoIcon />} onClick={() => { undoRedo('redo'); }}>Redo</Button>
<Button icon={<UndoIcon />} onClick={undo}>Undo</Button>
<Button icon={<RedoIcon />} onClick={redo}>Redo</Button>
<Button className="col-span-full" icon={<DeleteIcon />} onClick={() => { reset(); }} variant="danger">Reset</Button>
</div>
</>
Expand Down
Loading

0 comments on commit f5debba

Please sign in to comment.