Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 49 additions & 11 deletions app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
"use client";

import { useEffect } from "react";
import { useEffect, useState } from "react";
import { useRef } from "react";
import { useState } from "react";
import { usePreferencesStore } from "@/store/use-preferences-store";
import { fonts } from "@/options";
import { themes } from "@/options";
Expand All @@ -25,28 +24,67 @@ import ExportOptions from "@/components/controls/ExportOptions";
function App() {
const [width, setWidth] = useState("auto");
const [showWidth, setShowWidth] = useState(false);
const [isClient, setIsClient] = useState(false);

const store = usePreferencesStore();
const theme = usePreferencesStore((state) => state.theme);
const padding = usePreferencesStore((state) => state.padding);
const fontStyle = usePreferencesStore((state) => state.fontStyle);
const showBackground = usePreferencesStore((state) => state.showBackground);

const editorRef = useRef(null);

// Ensure we're on the client side
useEffect(() => {
setIsClient(true);
}, []);

useEffect(() => {
if (!isClient) return;

const queryParams = new URLSearchParams(location.search);
if (queryParams.size === 0) return;

const state = Object.fromEntries(queryParams);

// Use the store's setter methods instead of direct setState
if (state.code) {
store.setCode(atob(state.code));
}
if (state.autoDetectLanguage !== undefined) {
store.setAutoDetectLanguage(state.autoDetectLanguage === "true");
}
if (state.darkMode !== undefined) {
store.setDarkMode(state.darkMode === "true");
}
if (state.fontSize) {
store.setFontSize(Number(state.fontSize));
}
if (state.padding) {
store.setPadding(Number(state.padding));
}
if (state.language) {
store.setLanguage(state.language);
}
if (state.title) {
store.setTitle(state.title);
}
if (state.theme) {
store.setTheme(state.theme);
}
if (state.fontStyle) {
store.setFontStyle(state.fontStyle);
}
}, [isClient, store]);

usePreferencesStore.setState({
...state,
code: state.code ? atob(state.code) : "",
autoDetectLanguage: state.autoDetectLanguage === "true",
darkMode: state.darkMode === "true",
fontSize: Number(state.fontSize || 18),
padding: Number(state.padding || 64),
});
}, []);
// Don't render until we're on the client side
if (!isClient) {
return (
<main className="dark min-h-screen flex flex-col gap-4 justify-center items-center bg-neutral-950 text-white p-4">
<div className="animate-pulse">Loading...</div>
</main>
);
}

return (
<main className="dark min-h-screen flex flex-col gap-4 justify-center items-center bg-neutral-950 text-white p-4">
Expand Down
45 changes: 31 additions & 14 deletions components/CodeEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,48 @@ import { cn } from "@/lib/utils";
import flourite from "flourite";
import { codeSnippets, fonts } from "@/options";
import hljs from "highlight.js";
import { useEffect } from "react";
import { useEffect, useState } from "react";
import Editor from "react-simple-code-editor";
import { usePreferencesStore } from "@/store/use-preferences-store";

export default function CodeEditor() {
const store = usePreferencesStore();
const [isClient, setIsClient] = useState(false);

// Add random code snippets on mount
// Ensure we're on the client side to prevent hydration issues
useEffect(() => {
const randomSnippet =
codeSnippets[Math.floor(Math.random() * codeSnippets.length)];
usePreferencesStore.setState(randomSnippet);
setIsClient(true);
}, []);

// Add random code snippets on mount only once
useEffect(() => {
if (isClient && !store.code) {
const randomSnippet =
codeSnippets[Math.floor(Math.random() * codeSnippets.length)];
store.setCode(randomSnippet.code);
store.setLanguage(randomSnippet.language);
}
}, [isClient, store.code, store.setCode, store.setLanguage]);

// Auto Detect Language
useEffect(() => {
if (store.autoDetectLanguage) {
if (isClient && store.autoDetectLanguage && store.code) {
// use flourite to detect language and provide highlighting
const { language } = flourite(store.code, { noUnknown: true });
usePreferencesStore.setState({
language: language.toLowerCase() || "plaintext",
});
store.setLanguage(language.toLowerCase() || "plaintext");
}
}, [store.autoDetectLanguage, store.code]);
}, [isClient, store.autoDetectLanguage, store.code, store.setLanguage]);

// Don't render until we're on the client side
if (!isClient) {
return (
<div className="border-2 rounded-xl shadow-2xl bg-gray-100 animate-pulse">
<div className="h-64 flex items-center justify-center">
<div className="text-gray-500">Loading...</div>
</div>
</div>
);
}

return (
<div
Expand All @@ -46,9 +64,7 @@ export default function CodeEditor() {
<input
type="text"
value={store.title}
onChange={(e) =>
usePreferencesStore.setState({ title: e.target.value })
}
onChange={(e) => store.setTitle(e.target.value)}
spellCheck={false}
onClick={(e) => {
if (e.target instanceof HTMLInputElement) {
Expand All @@ -68,8 +84,9 @@ export default function CodeEditor() {
)}
>
<Editor
key={`editor-${store.language}-${store.fontStyle}`}
value={store.code}
onValueChange={(code) => usePreferencesStore.setState({ code })}
onValueChange={(code) => store.setCode(code)}
highlight={(code) =>
hljs.highlight(code, { language: store.language || "plaintext" })
.value
Expand Down
53 changes: 48 additions & 5 deletions components/controls/ExportOptions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,62 @@ import { toast } from "react-hot-toast";
import { toBlob, toPng, toSvg } from "html-to-image";
import { usePreferencesStore } from "@/store/use-preferences-store";
import { useHotkeys } from "react-hotkeys-hook";
import { fonts } from "@/options";

export default function ExportOptions({
targetRef,
}: {
targetRef: React.RefObject<HTMLDivElement>;
}) {
const title = usePreferencesStore((state) => state.title);
const fontStyle = usePreferencesStore((state) => state.fontStyle);

// Function to ensure fonts are loaded before export
const ensureFontsLoaded = async () => {
const currentFont = fonts[fontStyle as keyof typeof fonts];
if (currentFont && currentFont.src) {
try {
// Create a temporary link element to load the font
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = currentFont.src;
link.crossOrigin = 'anonymous';

// Wait for the font to load
await new Promise((resolve, reject) => {
link.onload = resolve;
link.onerror = reject;
document.head.appendChild(link);
});

// Wait a bit more to ensure font rendering
await new Promise(resolve => setTimeout(resolve, 100));
} catch (error) {
console.warn('Font loading failed, proceeding with export:', error);
}
}
};

// Common options for html-to-image to preserve fonts and quality
const exportOptions = {
pixelRatio: 2,
cacheBust: true,
// Ensure fonts are embedded
fontEmbedCSS: true,
useCORS: true,
// Add quality options
quality: 1.0
};

const copyImage = async () => {
const loading = toast.loading("Copying...");

try {
// Ensure fonts are loaded before export
await ensureFontsLoaded();

// generate blob from DOM node using html-to-image library
const imgBlob = await toBlob(targetRef.current, {
pixelRatio: 2,
});
const imgBlob = await toBlob(targetRef.current, exportOptions);

// Create a new ClipboardItem from the image blob
const img = new ClipboardItem({ "image/png": imgBlob as Blob });
Expand Down Expand Up @@ -70,14 +110,17 @@ export default function ExportOptions({
const loading = toast.loading(`Exporting ${format} image...`);

try {
// Ensure fonts are loaded before export
await ensureFontsLoaded();

let imgUrl, filename;
switch (format) {
case "PNG":
imgUrl = await toPng(targetRef.current, { pixelRatio: 2 });
imgUrl = await toPng(targetRef.current, exportOptions);
filename = `${name}.png`;
break;
case "SVG":
imgUrl = await toSvg(targetRef.current, { pixelRatio: 2 });
imgUrl = await toSvg(targetRef.current, exportOptions);
filename = `${name}.svg`;
break;

Expand Down
5 changes: 1 addition & 4 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,11 @@ const eslintConfig = [
"next/typescript",
"plugin:unicorn/recommended",
"plugin:import/recommended",
"plugin:tailwindcss/recommended",
),
{
rules: {
"simple-import-sort/exports": "error",
"simple-import-sort/imports": "error",
"tailwindcss/classnames-order": "error",
"tailwindcss/no-custom-classname": "off",
"unicorn/no-array-callback-reference": "off",
"unicorn/no-array-for-each": "off",
"unicorn/no-array-reduce": "off",
Expand All @@ -44,7 +41,7 @@ const eslintConfig = [
},
},
{
plugins: ["simple-import-sort", "tailwindcss"],
plugins: ["simple-import-sort"],
},
]

Expand Down
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@
"eslint-plugin-prettier": "^5.2.6",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-simple-import-sort": "^12.1.1",
"eslint-plugin-tailwindcss": "3.18.0",
"eslint-plugin-unicorn": "^58.0.0",
"prettier": "^3.5.3",
"prettier-plugin-tailwindcss": "^0.6.11",
Expand Down
4 changes: 4 additions & 0 deletions store/use-preferences-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ interface PreferencesState {
setCode: (code: string) => void;
setTitle: (title: string) => void;
setTheme: (theme: string) => void;
setDarkMode: (darkMode: boolean) => void;
toggleDarkMode: () => void;
setBackground: (showBackground: boolean) => void;
toggleBackground: () => void;
setLanguage: (language: string) => void;
setAutoDetectLanguage: (enabled: boolean) => void;
Expand Down Expand Up @@ -67,7 +69,9 @@ export const usePreferencesStore = create<PreferencesState>()(
setCode: (code) => set({ code }),
setTitle: (title) => set({ title }),
setTheme: (theme) => set({ theme }),
setDarkMode: (darkMode) => set({ darkMode }),
toggleDarkMode: () => set((state) => ({ darkMode: !state.darkMode })),
setBackground: (showBackground) => set({ showBackground }),
toggleBackground: () =>
set((state) => ({ showBackground: !state.showBackground })),
setLanguage: (language) => set({ language }),
Expand Down