Spaces:
Running
Running
"use client"; | |
import { Dice } from "@/components/dice"; | |
import { EmojiSelector } from "@/components/emoji-selector"; | |
import { GithubForkRibbon } from "@/components/github"; | |
import { | |
Select, | |
SelectContent, | |
SelectItem, | |
SelectTrigger, | |
SelectValue, | |
} from "@/components/ui/select"; | |
import { Slider } from "@/components/ui/slider"; | |
import { Toaster } from "@/components/ui/toaster"; | |
import { | |
Tooltip, | |
TooltipContent, | |
TooltipProvider, | |
TooltipTrigger, | |
} from "@/components/ui/tooltip"; | |
import { useToast } from "@/components/ui/use-toast"; | |
import { presetImage, presetArtStyles } from "@/util/presets"; | |
import { usePrevious } from "@/util/use-previous"; | |
import { useResponse } from "@/util/use-response"; | |
import { getShareUrl, Option, useShare } from "@/util/use-share"; | |
import { clsx } from "clsx"; | |
import { Check, Download, Share2 } from "lucide-react"; | |
import { useEffect, useMemo, useState } from "react"; | |
import { | |
FacebookIcon, | |
FacebookShareButton, | |
LinkedinIcon, | |
LinkedinShareButton, | |
TwitterShareButton, | |
XIcon, | |
} from "react-share"; | |
import { setEmojiFavicon } from "@/util/set-emoji-favicon"; | |
export default function TryEmoji() { | |
const { option: presetOption, hasShare } = useShare(); | |
const { toast } = useToast(); | |
const [emoji, setEmoji] = useState({ | |
emoji: presetOption.emoji, | |
name: presetOption.name, | |
}); | |
const [preset, setPreset] = useState( | |
presetArtStyles.find((p) => p.prompt === presetOption.prompt)!, | |
); | |
const [strength, setStrength] = useState(presetOption.strength); | |
const [seed, setSeed] = useState(presetOption.seed); | |
const shareOption: Option = useMemo(() => { | |
return { | |
emoji: emoji.emoji, | |
name: emoji.name, | |
prompt: preset.prompt, | |
seed: seed, | |
strength: strength, | |
}; | |
}, [emoji.emoji, emoji.name, preset.prompt, seed, strength]); | |
const { image, loading } = useResponse( | |
hasShare, | |
emoji.emoji, | |
emoji.name, | |
preset.prompt, | |
strength, | |
seed, | |
); | |
const previousImage = usePrevious(image); | |
const mergedImage = useMemo( | |
() => image || previousImage || presetImage, | |
[image, previousImage], | |
); | |
useEffect(() => { | |
setEmojiFavicon(emoji.emoji); | |
}, [emoji.emoji]); | |
const shareKey = useMemo(() => { | |
return getShareUrl(shareOption); | |
}, [shareOption]); | |
const shareUrl = useMemo(() => { | |
return `https://tryemoji.com?share=${shareKey}`; | |
}, [shareKey]); | |
const warmOrg: Promise<void> = useMemo(() => { | |
if (image) { | |
return fetch("/api/share", { | |
method: "POST", | |
body: JSON.stringify({ | |
image: image, | |
key: shareKey, | |
}), | |
}).then(); | |
} else { | |
return new Promise((resolve) => resolve()); | |
} | |
}, [image, shareKey]); | |
return ( | |
<TooltipProvider delayDuration={50}> | |
<Toaster /> | |
<div className="min-h-screen flex flex-col gap-4 bg-zinc-950 items-center justify-center py-4 md:py-12"> | |
<GithubForkRibbon></GithubForkRibbon> | |
<div className="text-6xl text-zinc-100"> | |
{emoji.emoji || "🐤"} tryEmoji{" "} | |
</div> | |
<div className="text-xl text-zinc-100"> | |
Turn emoji into amazing artwork via AI | |
</div> | |
<div className="flex items-center justify-center flex-col md:flex-row gap-2 md:gap-4"> | |
<div className="flex-0 w-full md:w-80"> | |
<EmojiSelector | |
onSelect={(e) => { | |
const prefix = | |
e.keywords.indexOf("animal") > -1 ? "super cute" : ""; | |
const keyword = e.keywords.join(", "); | |
const emoji = e.native; | |
const name = `${prefix} ${e.name}, ${keyword}`; | |
setEmoji({ emoji, name }); | |
}} | |
></EmojiSelector> | |
</div> | |
<div className="flex-1"> | |
<div className="max-w-[100vw] h-auto md:h-[512px] w-[512px] rounded-lg overflow-hidden relative"> | |
<img src={mergedImage} className="h-full w-full object-contain" /> | |
<div | |
className={clsx("transition absolute inset-0", { | |
"backdrop-blur-xl": loading, | |
})} | |
></div> | |
<div className="hidden absolute top-2 right-2 md:flex gap-2 items-center"> | |
<FacebookShareButton | |
beforeOnClick={() => warmOrg} | |
url={shareUrl} | |
> | |
<FacebookIcon className="rounded" size={24}></FacebookIcon> | |
</FacebookShareButton> | |
<TwitterShareButton onClick={() => warmOrg} url={shareUrl}> | |
<XIcon className="rounded" size={24} /> | |
</TwitterShareButton> | |
<LinkedinShareButton onClick={() => warmOrg} url={shareUrl}> | |
<LinkedinIcon className="rounded" size={24} /> | |
</LinkedinShareButton> | |
<Tooltip> | |
<TooltipTrigger asChild> | |
<button | |
onClick={() => { | |
warmOrg.then(() => { | |
navigator.clipboard.writeText(shareUrl).then(() => { | |
toast({ | |
description: ( | |
<div className="flex gap-2 text-sm items-center"> | |
<Check className="text-green-500"></Check> | |
Copied, paste to share | |
</div> | |
), | |
}); | |
}); | |
}); | |
}} | |
className="flex-0 rounded bg-amber-600 w-6 flex items-center justify-center h-6 text-sm font-semibold text-white shadow-sm hover:bg-amber-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-amber-600" | |
> | |
<Share2 size={16}></Share2> | |
</button> | |
</TooltipTrigger> | |
<TooltipContent> | |
<p>Share</p> | |
</TooltipContent> | |
</Tooltip> | |
</div> | |
<div className="absolute bottom-2 left-2 right-2 flex gap-2 flex-wrap"> | |
<div className="flex flex-auto gap-2 w-full md:w-auto"> | |
<div className="text-xl text-zinc-100">AI</div> | |
<Tooltip> | |
<TooltipTrigger asChild> | |
<Slider | |
className="flex-1" | |
defaultValue={[strength]} | |
onValueChange={(v) => setStrength(v[0])} | |
max={0.7} | |
min={0.5} | |
step={0.025} | |
/> | |
</TooltipTrigger> | |
<TooltipContent> | |
<p>AI strength</p> | |
</TooltipContent> | |
</Tooltip> | |
</div> | |
<div className="flex flex-auto md:flex-grow-0 gap-2 w-full md:w-auto"> | |
<Select | |
value={preset.artist} | |
onValueChange={(value) => | |
setPreset( | |
presetArtStyles.find((p) => p.artist === value)!, | |
) | |
} | |
> | |
<Tooltip> | |
<TooltipTrigger asChild> | |
<SelectTrigger className="flex-1 w-56 border-0 rounded bg-amber-600 px-2 py-1 text-sm font-semibold text-white shadow-sm hover:bg-amber-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-amber-600"> | |
<SelectValue placeholder="Select a fruit" /> | |
</SelectTrigger> | |
</TooltipTrigger> | |
<TooltipContent> | |
<p>Art style</p> | |
</TooltipContent> | |
</Tooltip> | |
<SelectContent> | |
{presetArtStyles.map((p) => ( | |
<SelectItem key={p.artist} value={p.artist}> | |
{p.artist} | |
</SelectItem> | |
))} | |
</SelectContent> | |
</Select> | |
<Tooltip> | |
<TooltipTrigger asChild> | |
<button | |
onClick={() => { | |
setSeed(Math.floor(Math.random() * 2159232)); | |
}} | |
className="flex-0 rounded bg-amber-600 px-0.5 py-0.5 text-sm font-semibold text-white shadow-sm hover:bg-amber-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-amber-600" | |
> | |
<Dice></Dice> | |
</button> | |
</TooltipTrigger> | |
<TooltipContent> | |
<p>Random</p> | |
</TooltipContent> | |
</Tooltip> | |
<Tooltip> | |
<TooltipTrigger asChild> | |
<a | |
href={image} | |
download | |
className="flex-0 block rounded bg-amber-600 px-0.5 py-0.5 text-sm font-semibold text-white shadow-sm hover:bg-amber-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-amber-600" | |
> | |
<Download /> | |
</a> | |
</TooltipTrigger> | |
<TooltipContent> | |
<p>Download</p> | |
</TooltipContent> | |
</Tooltip> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<div className="text-xs text-zinc-500 font-sans mt-8 flex gap-2"> | |
<a | |
className="hover:text-zinc-100" | |
href="https://lepton.ai" | |
target="_blank" | |
> | |
Powered by Lepton AI | |
</a> | |
| | |
<a | |
className="hover:text-zinc-100" | |
href="https://github.com/leptonai/tryemoji" | |
target="_blank" | |
> | |
Github | |
</a> | |
</div> | |
</div> | |
</TooltipProvider> | |
); | |
} | |