Merge branch 'main' into feat/add-image-comparison

This commit is contained in:
clean99 2023-11-22 12:04:45 +08:00 committed by GitHub
commit 219b241a67
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 81 additions and 43 deletions

View File

@ -12,8 +12,9 @@ See the [Examples](#examples) section below for more demos.
## 🌟 Recent Updates ## 🌟 Recent Updates
- Nov 21 - You can now edit code in the code editor and preview changes live thanks to [@clean99](https://github.com/clean99)
- Nov 20 - Paste in a URL to screenshot and clone (requires [ScreenshotOne free API key](https://screenshotone.com?via=screenshot-to-code)) - Nov 20 - Paste in a URL to screenshot and clone (requires [ScreenshotOne free API key](https://screenshotone.com?via=screenshot-to-code))
- Nov 19 - Support for dark/light code editor theme - thanks https://github.com/kachbit - Nov 19 - Support for dark/light code editor theme - thanks [@kachbit](https://github.com/kachbit)
- Nov 16 - Added a setting to disable DALL-E image generation if you don't need that - Nov 16 - Added a setting to disable DALL-E image generation if you don't need that
- Nov 16 - View code directly within the app - Nov 16 - View code directly within the app
- Nov 15 - 🔥 You can now instruct the AI to update the code as you wish. It is helpful if the AI messed up some styles or missed a section. - Nov 15 - 🔥 You can now instruct the AI to update the code as you wish. It is helpful if the AI messed up some styles or missed a section.

View File

@ -22,7 +22,7 @@ import { Textarea } from "@/components/ui/textarea";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "./components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "./components/ui/tabs";
import CodeMirror from "./components/CodeMirror"; import CodeMirror from "./components/CodeMirror";
import SettingsDialog from "./components/SettingsDialog"; import SettingsDialog from "./components/SettingsDialog";
import { AppStatus, Settings } from "@/types"; import { Settings, EditorTheme, AppState } from "./types";
import { IS_RUNNING_ON_CLOUD } from "./config"; import { IS_RUNNING_ON_CLOUD } from "./config";
import { PicoBadge } from "./components/PicoBadge"; import { PicoBadge } from "./components/PicoBadge";
import { OnboardingNote } from "./components/OnboardingNote"; import { OnboardingNote } from "./components/OnboardingNote";
@ -44,7 +44,8 @@ function App() {
openAiApiKey: null, openAiApiKey: null,
screenshotOneApiKey: null, screenshotOneApiKey: null,
isImageGenerationEnabled: true, isImageGenerationEnabled: true,
editorTheme: "cobalt", editorTheme: EditorTheme.COBALT,
isTermOfServiceAccepted: false,
}, },
"setting" "setting"
); );
@ -150,10 +151,22 @@ function App() {
toast.success("Copied to clipboard"); toast.success("Copied to clipboard");
}, [generatedCode]); }, [generatedCode]);
const handleTermDialogOpenChange = (open: boolean) => {
setSettings((s) => ({
...s,
isTermOfServiceAccepted: !open,
}));
};
return ( return (
<div className="mt-2"> <div className="mt-2">
{IS_RUNNING_ON_CLOUD && <PicoBadge />} {IS_RUNNING_ON_CLOUD && <PicoBadge />}
{IS_RUNNING_ON_CLOUD && <TermsOfServiceDialog />} {IS_RUNNING_ON_CLOUD && (
<TermsOfServiceDialog
open={!settings.isTermOfServiceAccepted}
onOpenChange={handleTermDialogOpenChange}
/>
)}
<div className="hidden lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:w-96 lg:flex-col"> <div className="hidden lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:w-96 lg:flex-col">
<div className="flex grow flex-col gap-y-2 overflow-y-auto border-r border-gray-200 bg-white px-6"> <div className="flex grow flex-col gap-y-2 overflow-y-auto border-r border-gray-200 bg-white px-6">
@ -301,6 +314,7 @@ function App() {
<CodeMirror <CodeMirror
code={generatedCode} code={generatedCode}
editorTheme={settings.editorTheme} editorTheme={settings.editorTheme}
onCodeChange={setGeneratedCode}
/> />
<span <span
title="Copy Code" title="Copy Code"

View File

@ -1,6 +1,6 @@
import { useRef, useEffect } from "react"; import { useRef, useEffect, useMemo } from "react";
import { EditorState } from "@codemirror/state"; import { EditorState } from "@codemirror/state";
import { EditorView, keymap, lineNumbers } from "@codemirror/view"; import { EditorView, keymap, lineNumbers, ViewUpdate } from "@codemirror/view";
import { espresso, cobalt } from "thememirror"; import { espresso, cobalt } from "thememirror";
import { import {
defaultKeymap, defaultKeymap,
@ -11,39 +11,42 @@ import {
} from "@codemirror/commands"; } from "@codemirror/commands";
import { bracketMatching } from "@codemirror/language"; import { bracketMatching } from "@codemirror/language";
import { html } from "@codemirror/lang-html"; import { html } from "@codemirror/lang-html";
import { EditorTheme } from "@/types";
interface Props { interface Props {
code: string; code: string;
editorTheme: string; editorTheme: EditorTheme;
onCodeChange: (code: string) => void;
} }
function CodeMirror({ code, editorTheme }: Props) { function CodeMirror({ code, editorTheme, onCodeChange }: Props) {
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
const view = useRef<EditorView | null>(null); const view = useRef<EditorView | null>(null);
const editorState = useMemo(() => EditorState.create({
useEffect(() => { extensions: [
let selectedTheme = cobalt; history(),
if (editorTheme === "espresso") { keymap.of([
selectedTheme = espresso; ...defaultKeymap,
} indentWithTab,
view.current = new EditorView({ { key: "Mod-z", run: undo, preventDefault: true },
state: EditorState.create({ { key: "Mod-Shift-z", run: redo, preventDefault: true },
doc: code, ]),
extensions: [ lineNumbers(),
history(), bracketMatching(),
keymap.of([ html(),
...defaultKeymap, editorTheme === EditorTheme.ESPRESSO ? espresso : cobalt,
indentWithTab, EditorView.lineWrapping,
{ key: "Mod-z", run: undo, preventDefault: true }, EditorView.updateListener.of((update: ViewUpdate) => {
{ key: "Mod-Shift-z", run: redo, preventDefault: true }, if (update.docChanged) {
]), const updatedCode = update.state.doc.toString();
lineNumbers(), onCodeChange(updatedCode);
bracketMatching(), }
html(),
selectedTheme,
EditorView.lineWrapping,
],
}), }),
],
}), [editorTheme]);
useEffect(() => {
view.current = new EditorView({
state: editorState,
parent: ref.current as Element, parent: ref.current as Element,
}); });
@ -53,7 +56,7 @@ function CodeMirror({ code, editorTheme }: Props) {
view.current = null; view.current = null;
} }
}; };
}, [code, editorTheme]); }, []);
useEffect(() => { useEffect(() => {
if (view.current && view.current.state.doc.toString() !== code) { if (view.current && view.current.state.doc.toString() !== code) {
@ -69,3 +72,4 @@ function CodeMirror({ code, editorTheme }: Props) {
} }
export default CodeMirror; export default CodeMirror;

View File

@ -1,3 +1,4 @@
import { useEffect, useRef } from 'react';
import classNames from "classnames"; import classNames from "classnames";
import useThrottle from "../hooks/useThrottle"; import useThrottle from "../hooks/useThrottle";
@ -8,13 +9,23 @@ interface Props {
function Preview({ code, device }: Props) { function Preview({ code, device }: Props) {
const throttledCode = useThrottle(code, 200); const throttledCode = useThrottle(code, 200);
const iframeRef = useRef<HTMLIFrameElement | null>(null);
useEffect(() => {
const iframe = iframeRef.current;
if (iframe && iframe.contentDocument) {
iframe.contentDocument.open();
iframe.contentDocument.write(throttledCode);
iframe.contentDocument.close();
}
}, [throttledCode]);
return ( return (
<div className="flex justify-center mx-2"> <div className="flex justify-center mx-2">
<iframe <iframe
id={`preview-${device}`} id={`preview-${device}`}
ref={iframeRef}
title="Preview" title="Preview"
srcDoc={throttledCode}
className={classNames( className={classNames(
"border-[4px] border-black rounded-[20px] shadow-lg", "border-[4px] border-black rounded-[20px] shadow-lg",
"transform scale-[0.9] origin-top", "transform scale-[0.9] origin-top",
@ -27,4 +38,5 @@ function Preview({ code, device }: Props) {
</div> </div>
); );
} }
export default Preview; export default Preview;

View File

@ -9,7 +9,7 @@ import {
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { FaCog } from "react-icons/fa"; import { FaCog } from "react-icons/fa";
import { Settings } from "../types"; import { EditorTheme, Settings } from "../types";
import { Switch } from "./ui/switch"; import { Switch } from "./ui/switch";
import { Label } from "./ui/label"; import { Label } from "./ui/label";
import { Input } from "./ui/input"; import { Input } from "./ui/input";
@ -21,7 +21,7 @@ interface Props {
} }
function SettingsDialog({ settings, setSettings }: Props) { function SettingsDialog({ settings, setSettings }: Props) {
const handleThemeChange = (theme: string) => { const handleThemeChange = (theme: EditorTheme) => {
setSettings((s) => ({ setSettings((s) => ({
...s, ...s,
editorTheme: theme, editorTheme: theme,
@ -110,7 +110,7 @@ function SettingsDialog({ settings, setSettings }: Props) {
id="editor-theme" id="editor-theme"
value={settings.editorTheme} value={settings.editorTheme}
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => onChange={(e: React.ChangeEvent<HTMLSelectElement>) =>
handleThemeChange(e.target.value) handleThemeChange(e.target.value as EditorTheme)
} }
> >
<option value="cobalt">Cobalt</option> <option value="cobalt">Cobalt</option>

View File

@ -6,13 +6,14 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { useState } from "react";
function TermsOfServiceDialog() { const TermsOfServiceDialog: React.FC<{
const [isOpen, setIsOpen] = useState(true); open: boolean;
onOpenChange: (open: boolean) => void;
}> = ({ open, onOpenChange }) => {
return ( return (
<Dialog open={isOpen} onOpenChange={setIsOpen}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle className="mb-4">Terms of Service</DialogTitle> <DialogTitle className="mb-4">Terms of Service</DialogTitle>
@ -45,6 +46,6 @@ function TermsOfServiceDialog() {
</DialogContent> </DialogContent>
</Dialog> </Dialog>
); );
} };
export default TermsOfServiceDialog; export default TermsOfServiceDialog;

View File

@ -1,8 +1,14 @@
export enum EditorTheme {
ESPRESSO = "espresso",
COBALT = "cobalt",
}
export interface Settings { export interface Settings {
openAiApiKey: string | null; openAiApiKey: string | null;
screenshotOneApiKey: string | null; screenshotOneApiKey: string | null;
isImageGenerationEnabled: boolean; isImageGenerationEnabled: boolean;
editorTheme: string; editorTheme: EditorTheme;
isTermOfServiceAccepted: boolean; // Only relevant for hosted version
} }
export enum AppState { export enum AppState {