Skip to content

Commit

Permalink
feat: add splash (#15)
Browse files Browse the repository at this point in the history
* feat: templates

* refactor: single page

* fix: fully static

* feat: improve splash
  • Loading branch information
QuiiBz authored Dec 28, 2023
1 parent 0a616aa commit 67a0213
Show file tree
Hide file tree
Showing 21 changed files with 337 additions and 93 deletions.
3 changes: 2 additions & 1 deletion apps/dashboard/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ const nextConfig = {
},
typescript: {
ignoreBuildErrors: true,
}
},
reactStrictMode: false,
}

module.exports = nextConfig
9 changes: 5 additions & 4 deletions apps/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,18 @@
"next": "14.0.4",
"react": "^18",
"react-dom": "^18",
"react-promise-suspense": "^0.3.4",
"satori": "^0.10.11"
},
"devDependencies": {
"@next/eslint-plugin-next": "^14.0.3",
"@ogstudio/eslint": "workspace:*",
"@ogstudio/typescript": "workspace:*",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"autoprefixer": "^10.0.1",
"@next/eslint-plugin-next": "^14.0.3",
"postcss": "^8",
"tailwindcss": "^3.3.0",
"@ogstudio/eslint": "workspace:*",
"@ogstudio/typescript": "workspace:*"
"tailwindcss": "^3.3.0"
}
}
16 changes: 16 additions & 0 deletions apps/dashboard/src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,22 @@
@tailwind components;
@tailwind utilities;

/**
* https://github.com/tailwindlabs/tailwindcss/discussions/2394
* https://github.com/tailwindlabs/tailwindcss/pull/5732
*/
@layer utilities {
/* Chrome, Safari and Opera */
.no-scrollbar::-webkit-scrollbar {
display: none;
}

.no-scrollbar {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
}

.element:focus {
outline: none;
}
Expand Down
5 changes: 5 additions & 0 deletions apps/dashboard/src/app/my-images/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default function Templates() {
return (
<p>Todo</p>
)
}
34 changes: 7 additions & 27 deletions apps/dashboard/src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,12 @@
'use client'
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: createElementId(),
tag: 'div',
name: 'Box',
x: 0,
y: 0,
width: 1200,
height: 630,
visible: true,
rotate: 0,
opacity: 100,
backgroundColor: '#ffffff',
},
]
import { Suspense } from "react";
import { OgSplash } from "../components/OgSplash";

export default function Home() {
return (
<OgEditor height={630} initialElements={initialElements} width={1200} />
// 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 />
</Suspense>
)
}
5 changes: 5 additions & 0 deletions apps/dashboard/src/app/templates/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default function Templates() {
return (
<p>Todo</p>
)
}
19 changes: 0 additions & 19 deletions apps/dashboard/src/components/Button.tsx

This file was deleted.

20 changes: 20 additions & 0 deletions apps/dashboard/src/components/CustomLink.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import Link from "next/link"
import type { ReactNode } from "react"

interface CustomLinkProps {
icon?: ReactNode
iconPosition?: 'left' | 'right'
href: string
children: ReactNode
}

export function CustomLink({ icon, iconPosition = 'left', href, children }: CustomLinkProps) {
return (
<Link className="flex gap-2 items-center px-3 py-1 rounded-full text-gray-600 hover:text-gray-900 hover:bg-gray-50" href={href}>
{icon && iconPosition === 'left' ? icon : null}
{children}
{icon && iconPosition === 'right' ? icon : null}
</Link>
)
}

25 changes: 3 additions & 22 deletions apps/dashboard/src/components/LeftPanel/ExportSection.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,16 @@
import { useState } from "react";
import { flushSync } from "react-dom";
import { Button } from "../Button";
import { Button } from "../forms/Button";
import { PngIcon } from "../icons/PngIcon";
import { SvgIcon } from "../icons/SvgIcon";
import { useOg } from "../OgEditor";
import { domToReactLike, exportToPng, exportToSvg } from "../../lib/export";
import { loadFonts } from "../../lib/fonts";

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

async function getFonts() {
return Promise.all(elements.filter(element => element.tag === 'p' || element.tag === 'span').map(async element => {
// @ts-expect-error -- wrong inference
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access -- wrong inference
const fontName = element.fontFamily.toLowerCase().replace(' ', '-')
// @ts-expect-error -- wrong inference
const data = await fetch(`https://fonts.bunny.net/${fontName}/files/${fontName}-latin-${element.fontWeight}-normal.woff`).then(response => response.arrayBuffer())

return {
// @ts-expect-error -- wrong inference
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- wrong inference
name: element.fontFamily,
data,
// @ts-expect-error -- wrong inference
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- wrong inference
weight: element.fontWeight,
}
}))
}

async function exportSvg(openInNewTab = true) {
if (!rootRef.current) {
return {}
Expand All @@ -42,7 +23,7 @@ export function ExportSection() {
setIsLoading(true)

const reactLike = domToReactLike(rootRef.current, 'This is a dynamic text')
const fonts = await getFonts()
const fonts = await loadFonts(elements)
const svg = await exportToSvg(reactLike, fonts)

setIsLoading(false)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Button } from "../Button";
import { Button } from "../forms/Button";
import { useOg } from "../OgEditor";
import { DeleteIcon } from "../icons/DeleteIcon";
import { RedoIcon } from "../icons/RedoIcon";
Expand Down
12 changes: 12 additions & 0 deletions apps/dashboard/src/components/LeftPanel/SplashSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { CustomLink } from "../CustomLink";
import { ArrowLeftIcon } from "../icons/ArrowLeftIcon";

export function SplashSection() {
return (
<CustomLink href="/" icon={<ArrowLeftIcon />}>
Back
</CustomLink>
)
}


3 changes: 3 additions & 0 deletions apps/dashboard/src/components/LeftPanel/index.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { ExportSection } from "./ExportSection";
import { ModificationSection } from "./ModificationsSection";
import { ElementsSection } from "./ElementsSection";
import { SplashSection } from "./SplashSection";

export function LeftPanel() {
return (
<div className="flex flex-col items-start gap-4 p-4">
<SplashSection />
<div className="h-[1px] w-full bg-gray-100" />
<ElementsSection />
<div className="h-[1px] w-full bg-gray-100" />
<ModificationSection />
Expand Down
41 changes: 23 additions & 18 deletions apps/dashboard/src/components/OgEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
'use client'
import type { RefObject } from "react";
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
import type { OGElement } from "../lib/types";
Expand Down Expand Up @@ -35,6 +36,7 @@ export function useOg() {

interface OgProviderProps {
initialElements: OGElement[]
localStorageKey: string
width: number
height: number
}
Expand All @@ -44,17 +46,10 @@ let editIndex = -1

let elementToCopy: OGElement | undefined

export function OgEditor({ initialElements, width, height }: OgProviderProps) {
export function OgEditor({ initialElements, localStorageKey: key, width, height }: OgProviderProps) {
const localStorageKey = `og-${key}`
const [selectedElement, setRealSelectedElement] = useState<string | null>(null)
const [elements, setRealElements] = useState<OGElement[]>(() => {
const item = typeof localStorage !== 'undefined' ? localStorage.getItem('elements') : undefined

if (item) {
return JSON.parse(item) as OGElement[]
}

return initialElements
})
const [elements, setRealElements] = useState<OGElement[]>([])
const rootRef = useRef<HTMLDivElement>(null)

const setSelectedElement = useCallback((id: string | null) => {
Expand Down Expand Up @@ -84,8 +79,8 @@ export function OgEditor({ initialElements, width, height }: OgProviderProps) {
return newElements
})

localStorage.setItem('elements', JSON.stringify(newElements))
}, [])
localStorage.setItem(localStorageKey, JSON.stringify(newElements))
}, [localStorageKey])

const updateElement = useCallback((element: OGElement) => {
const index = elements.findIndex(item => item.id === element.id)
Expand Down Expand Up @@ -141,15 +136,25 @@ export function OgEditor({ initialElements, width, height }: OgProviderProps) {
}, [setElements])

/**
* Immediately load fonts for elements that are visible on the page.
* When the editor image is updated or loaded for the first time, reset every
* state, and load the elements and fonts.
*/
useEffect(() => {
elements.forEach(element => {
const item = localStorage.getItem(localStorageKey)
const ogElements = item ? JSON.parse(item) as OGElement[] : initialElements

setRealElements(ogElements)
setSelectedElement(null)
edits.length = 0
editIndex = -1

// Immediately load fonts for elements that will be visible on the page.
ogElements.forEach(element => {
if (element.tag === 'p' || element.tag === 'span') {
maybeLoadFont(element.fontFamily, element.fontWeight)
}
})
}, [])
}, [localStorageKey, initialElements])

useEffect(() => {
function onContextMenu(event: MouseEvent) {
Expand Down Expand Up @@ -253,10 +258,10 @@ export function OgEditor({ initialElements, width, height }: OgProviderProps) {
return (
<OgContext.Provider value={value}>
<div className="w-screen h-screen flex flex-row justify-between items-center bg-gray-50 overflow-hidden">
<div className="w-[300px] h-screen flex flex-col border-r border-gray-100 shadow-lg shadow-gray-100 bg-white z-10">
<div className="w-[300px] min-w-[300px] h-screen flex flex-col border-r border-gray-100 shadow-lg shadow-gray-100 bg-white z-10">
<LeftPanel />
</div>
<div className="flex flex-col items-center gap-4">
<div className="flex flex-col items-center gap-4 absolute transform left-1/2 -translate-x-1/2">
<p className="text-xs text-gray-400 z-10">{width}x{height}</p>
<div className="bg-white shadow-lg shadow-gray-100 relative" style={{ width, height }}>
<div ref={rootRef} style={{ display: 'flex', width: '100%', height: '100%' }}>
Expand All @@ -268,7 +273,7 @@ export function OgEditor({ initialElements, width, height }: OgProviderProps) {
<div className="border border-gray-100 absolute pointer-events-none transform translate-y-[32px]" style={{ width, height }} />
<EditorToolbar />
</div>
<div className="w-[300px] h-screen flex flex-col border-l border-gray-100 shadow-lg shadow-gray-100 bg-white z-10">
<div className="w-[300px] min-w-[300px] h-screen flex flex-col border-l border-gray-100 shadow-lg shadow-gray-100 bg-white z-10">
<RightPanel />
</div>
</div>
Expand Down
39 changes: 39 additions & 0 deletions apps/dashboard/src/components/OgImage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import usePromise from "react-promise-suspense"
import { createElementStyle } from "../lib/elements"
import { exportToSvg } from "../lib/export"
import { loadFonts } from "../lib/fonts"
import type { OGElement } from "../lib/types"

async function loadOgImage(elements: OGElement[]) {
const fonts = await loadFonts(elements)
const reactLike = {
type: 'div',
props: {
style: {
display: 'flex',
width: '100%',
height: '100%',
},
children: elements.map(element => ({
type: element.tag,
props: {
style: createElementStyle(element),
...(element.tag === 'p' ? { children: element.content } : {}),
},
}))
}
}

const svg = await exportToSvg(reactLike, fonts)
return `data:image/svg+xml;base64,${btoa(svg)}`
}

interface OgImageProps {
elements: OGElement[]
}

export function OgImage({ elements }: OgImageProps) {
const src = usePromise(loadOgImage, [elements])

return <img alt="" src={src} />
}
Loading

0 comments on commit 67a0213

Please sign in to comment.