Skip to content

Commit

Permalink
add custom fonts support
Browse files Browse the repository at this point in the history
  • Loading branch information
rgodha24 committed Aug 26, 2024
1 parent 03ee720 commit 3fe0cae
Show file tree
Hide file tree
Showing 8 changed files with 144 additions and 52 deletions.
2 changes: 2 additions & 0 deletions apps/dashboard/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.vercel
.env*.local
8 changes: 8 additions & 0 deletions apps/dashboard/src/app/api/fonts/route.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { getFontData } from "../../../lib/fonts";

export const revalidate = "force-cache"
export const runtime = "nodejs"

export async function GET() {
return Response.json(await getFontData())
}
4 changes: 2 additions & 2 deletions apps/dashboard/src/components/FontPreview.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { useEffect } from "react";
import { maybeLoadFont, type Font } from "../lib/fonts";
import { maybeLoadFont } from "../lib/fonts";

interface FontPreviewProps {
font: Font;
font: string;
}

export function FontPreview({ font }: FontPreviewProps) {
Expand Down
38 changes: 28 additions & 10 deletions apps/dashboard/src/components/RightPanel/FontSection.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { Flex, Grid, Text, Select, TextField, Tooltip } from "@radix-ui/themes";
import type { OGElement } from "../../lib/types";
import type { Font } from "../../lib/fonts";
import { FONTS, FONT_WEIGHTS } from "../../lib/fonts";
import { FontSizeIcon } from "../icons/FontSizeIcon";
import { LineHeightIcon } from "../icons/LineHeightIcon";
import { LetterSpacingIcon } from "../icons/LetterSpacingIcon";
import { useElementsStore } from "../../stores/elementsStore";
import { ColorPicker } from "../ColorPicker";
import { FontPreview } from "../FontPreview";
import { useFontsStore } from "../../stores/fontsStore";

const SPACES_REGEX = /\s+/g;

Expand All @@ -17,6 +16,7 @@ interface FontSectionProps {

export function FontSection({ selectedElement }: FontSectionProps) {
const updateElement = useElementsStore((state) => state.updateElement);
const { allFonts, installedFonts, installFont } = useFontsStore(({ allFonts, installedFonts, installFont }) => ({ allFonts, installedFonts, installFont }));

if (selectedElement.tag !== "p" && selectedElement.tag !== "span") {
return null;
Expand All @@ -28,12 +28,17 @@ export function FontSection({ selectedElement }: FontSectionProps) {
<Grid columns="2" gap="2">
<Select.Root
onValueChange={(value) => {
const font = value as unknown as Font;
const font = value;
const weights = allFonts.find((f) => f.name === font)?.weights;

if (!installedFonts.has(font)) {
installFont(font);
}

updateElement({
...selectedElement,
fontFamily: font,
fontWeight: FONT_WEIGHTS[font].includes(
fontWeight: weights?.includes(
selectedElement.fontWeight,
)
? selectedElement.fontWeight
Expand All @@ -44,13 +49,26 @@ export function FontSection({ selectedElement }: FontSectionProps) {
>
<Select.Trigger color="gray" variant="soft" />
<Select.Content variant="soft">
{FONTS.map((font) => (
<Select.Item key={font} value={font}>
<FontPreview font={font} />
</Select.Item>
))}
<Select.Group>
<Select.Label>Installed fonts</Select.Label>
{Array.from(installedFonts.values()).map((font) => (
<Select.Item key={font} value={font}>
<FontPreview font={font} />
</Select.Item>
))}
</Select.Group>
<Select.Separator />
<Select.Group>
<Select.Label>All fonts (from FontSource)</Select.Label>
{allFonts.filter((font) => !installedFonts.has(font.name)).map((font) => (
<Select.Item key={font.name} value={font.name}>
<FontPreview font={font.name} />
</Select.Item>
))}
</Select.Group>
</Select.Content>
</Select.Root>

<Select.Root
onValueChange={(value) => {
updateElement({
Expand All @@ -62,7 +80,7 @@ export function FontSection({ selectedElement }: FontSectionProps) {
>
<Select.Trigger color="gray" variant="soft" />
<Select.Content variant="soft">
{FONT_WEIGHTS[selectedElement.fontFamily].map((weight) => (
{allFonts.find(({ name }) => name === selectedElement.fontFamily)?.weights?.map((weight) => (
<Select.Item key={weight} value={String(weight)}>
{weight}
</Select.Item>
Expand Down
109 changes: 72 additions & 37 deletions apps/dashboard/src/lib/fonts.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { OGElement } from "./types";
import { unstable_cache } from "next/cache";

export const FONTS = [
export const DEFAULT_FONTS = [
"Roboto",
"Open Sans",
"Montserrat",
Expand All @@ -11,43 +12,42 @@ export const FONTS = [
"Raleway",
"Nunito",
"Ubuntu",
] as const;

export type Font = (typeof FONTS)[number];

export const FONT_WEIGHTS = {
Roboto: [100, 300, 400, 500, 700, 900],
"Open Sans": [300, 400, 600, 700, 800],
Montserrat: [100, 200, 300, 400, 500, 600, 700, 800, 900],
Lato: [100, 300, 400, 700, 900],
Poppins: [100, 200, 300, 400, 500, 600, 700, 800, 900],
Inter: [100, 200, 300, 400, 500, 600, 700, 800, 900],
Oswald: [200, 300, 400, 500, 600, 700],
Raleway: [100, 200, 300, 400, 500, 600, 700, 800, 900],
Nunito: [200, 300, 400, 500, 600, 700, 800, 900],
Ubuntu: [300, 400, 500, 700],
} satisfies Record<Font, number[]>;
];

/**
* Try to load a font from Bunny Fonts, if the font is not already loaded.
* Adds the font stylesheet to the document body
* The font is loaded asynchronously, so it may not be available immediately
* and the caller should make sure to wait for the font to be loaded before
* using it.
*/
export function maybeLoadFont(font: string, weight: number) {
const id = `font-${font}-${weight}`;
const fontID = font.toLowerCase().replaceAll(" ", "-");
const fontURL = getFontURL(font, weight);

if (document.getElementById(id)) {
return;
// Check if we've already added this font
if (document.getElementById(fontID)) {
return; // Font style has already been added, no need to add it again
}

const link = document.createElement("link");
link.id = id;
link.rel = "stylesheet";
link.href = `https://fonts.bunny.net/css?family=${font
.toLowerCase()
.replace(" ", "-")}:${weight}`;
document.head.appendChild(link);
// Create a style element
const style = document.createElement("style");
style.id = fontID;

// Define the @font-face rule
const fontFace = `
@font-face {
font-family: "${font}";
src: url("${fontURL}") format("woff");
font-weight: ${weight};
font-style: normal;
}
`;

// Add the @font-face rule to the style element
style.appendChild(document.createTextNode(fontFace));

// Append the style element to the head of the document
document.head.appendChild(style);
}

export interface FontData {
Expand Down Expand Up @@ -76,21 +76,15 @@ export async function loadFonts(elements: OGElement[]): Promise<FontData[]> {
return fontCache;
}

// @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(" ", "-");
if (element.tag !== "p" && element.tag !== "span") throw "unreachable!";

const data = await fetch(
// @ts-expect-error -- wrong inference
`https://fonts.bunny.net/${fontName}/files/${fontName}-latin-${element.fontWeight}-normal.woff`,
getFontURL(element.fontFamily, element.fontWeight),
).then((response) => response.arrayBuffer());

const fontData: FontData = {
// @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,
};

Expand All @@ -99,3 +93,44 @@ export async function loadFonts(elements: OGElement[]): Promise<FontData[]> {
}),
);
}

async function getFontDataInternal() {
interface FontsourceFont {
id: string;
family: string;
subsets: string[];
weights: number[];
styles: string[];
defSubset: string;
variable: boolean;
lastModified: Date;
category: string;
license: string;
type: string;
}

const res = await fetch("https://api.fontsource.org/v1/fonts", {
cache: "no-store",
});

const data: FontsourceFont[] = await res.json();

return data
.filter(({ styles }) => styles.includes("normal"))
.filter(({ defSubset }) => defSubset === "latin")
.map((font) => ({
name: font.family,
weights: font.weights,
}));
}

export const getFontData = unstable_cache(getFontDataInternal, ["font-data"], {
revalidate: 60 * 60 * 24 * 7,
});

export type Font = Awaited<ReturnType<typeof getFontData>>[number];

export function getFontURL(fontName: string, weight: number) {
const fontID = fontName.toLowerCase().replaceAll(" ", "-");
return `https://cdn.jsdelivr.net/fontsource/fonts/${fontID}@latest/latin-${weight}-normal.woff`;
}
4 changes: 1 addition & 3 deletions apps/dashboard/src/lib/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import type { Font } from "./fonts";

export type OGElement = (OGPElement | OGDynamicElement | OGDivElement) & {
id: string;
name: string;
Expand Down Expand Up @@ -28,7 +26,7 @@ export interface OGPElement {
tag: "p";
content: string;
color: string;
fontFamily: Font;
fontFamily: string;
fontWeight: number;
fontSize: number;
lineHeight: number;
Expand Down
2 changes: 2 additions & 0 deletions apps/dashboard/src/stores/elementsStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { create } from "zustand";
import { temporal } from "zundo";
import type { OGElement } from "../lib/types";
import { maybeLoadFont } from "../lib/fonts";
import { useFontsStore } from "./fontsStore";

interface ElementsState {
imageId: string;
Expand Down Expand Up @@ -34,6 +35,7 @@ export const useElementsStore = create<ElementsState>()(
elements.forEach((element) => {
if (element.tag === "p" || element.tag === "span") {
maybeLoadFont(element.fontFamily, element.fontWeight);
useFontsStore.getState().installFont(element.fontFamily);
}
});

Expand Down
29 changes: 29 additions & 0 deletions apps/dashboard/src/stores/fontsStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { create } from "zustand";
import { DEFAULT_FONTS, Font, maybeLoadFont } from "../lib/fonts";

interface FontsState {
installedFonts: Set<string>;
/** install the font based on its name */
installFont: (font: string) => void;
allFonts: Font[];
}

if (typeof window !== "undefined") {
fetch("/api/fonts")
.then((res) => res.json())
.then((fonts) => {
useFontsStore.getState().allFonts = fonts;
});
}

export const useFontsStore = create<FontsState>((set) => ({
installedFonts: new Set(DEFAULT_FONTS),
allFonts: [],
installFont: (name) => {
set((state) => {
state.installedFonts.add(name);

return state;
});
},
}));

0 comments on commit 3fe0cae

Please sign in to comment.