feat: add web
Browse files- .gitignore +38 -0
- README.md +47 -1
- index.html +2 -9
- next.config.js +4 -0
- package-lock.json +0 -0
- package.json +50 -0
- photon/main.py +216 -0
- postcss.config.js +6 -0
- requirements.txt +6 -0
- src/app/api/run/route.ts +22 -0
- src/app/api/share/route.ts +50 -0
- src/app/globals.css +27 -0
- src/app/icon.svg +1 -0
- src/app/layout.tsx +48 -0
- src/app/og/image.png +0 -0
- src/app/og/route.tsx +106 -0
- src/app/page.tsx +37 -0
- src/components/dice.tsx +44 -0
- src/components/emoji-selector.tsx +267 -0
- src/components/github.tsx +17 -0
- src/components/try-emoji.tsx +275 -0
- src/components/ui/select.tsx +160 -0
- src/components/ui/slider.tsx +28 -0
- src/components/ui/toast.tsx +127 -0
- src/components/ui/toaster.tsx +35 -0
- src/components/ui/tooltip.tsx +30 -0
- src/components/ui/use-toast.tsx +189 -0
- src/components/ui/utils.ts +6 -0
- src/util/presets.ts +45 -0
- src/util/set-emoji-favicon.ts +12 -0
- src/util/use-previous.ts +11 -0
- src/util/use-response.ts +76 -0
- src/util/use-share.ts +50 -0
- style.css +11 -23
- tailwind.config.ts +36 -0
- tsconfig.json +27 -0
.gitignore
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
| 2 |
+
|
| 3 |
+
# dependencies
|
| 4 |
+
/node_modules
|
| 5 |
+
/.pnp
|
| 6 |
+
.pnp.js
|
| 7 |
+
.yarn/install-state.gz
|
| 8 |
+
|
| 9 |
+
# testing
|
| 10 |
+
/coverage
|
| 11 |
+
|
| 12 |
+
# next.js
|
| 13 |
+
/.next/
|
| 14 |
+
/out/
|
| 15 |
+
|
| 16 |
+
# production
|
| 17 |
+
/build
|
| 18 |
+
|
| 19 |
+
# misc
|
| 20 |
+
.idea
|
| 21 |
+
photon/__pycache__/
|
| 22 |
+
.DS_Store
|
| 23 |
+
*.pem
|
| 24 |
+
|
| 25 |
+
# debug
|
| 26 |
+
npm-debug.log*
|
| 27 |
+
yarn-debug.log*
|
| 28 |
+
yarn-error.log*
|
| 29 |
+
|
| 30 |
+
# local env files
|
| 31 |
+
.env*.local
|
| 32 |
+
|
| 33 |
+
# vercel
|
| 34 |
+
.vercel
|
| 35 |
+
|
| 36 |
+
# typescript
|
| 37 |
+
*.tsbuildinfo
|
| 38 |
+
next-env.d.ts
|
README.md
CHANGED
|
@@ -7,5 +7,51 @@ sdk: static
|
|
| 7 |
pinned: false
|
| 8 |
license: mit
|
| 9 |
---
|
|
|
|
|
|
|
| 10 |
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
pinned: false
|
| 8 |
license: mit
|
| 9 |
---
|
| 10 |
+
<div align="center">
|
| 11 |
+
<h1 align="center">🐱 tryEmoji</h1>
|
| 12 |
|
| 13 |
+
Turn emoji into amazing artwork via AI
|
| 14 |
+
|
| 15 |
+
<a href="https://tryemoji.com">
|
| 16 |
+
<img src="https://tryemoji.com/preview.png">
|
| 17 |
+
</a>
|
| 18 |
+
</div>
|
| 19 |
+
|
| 20 |
+
## Features
|
| 21 |
+
|
| 22 |
+
- Includes complete front-end and back-end code.
|
| 23 |
+
- Support deployment both locally and in the cloud.
|
| 24 |
+
- Fully based on open source and can be used for commercial purposes.
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
## Install
|
| 28 |
+
|
| 29 |
+
```bash
|
| 30 |
+
# Install web dependencies
|
| 31 |
+
npm install
|
| 32 |
+
|
| 33 |
+
# Install server dependencies
|
| 34 |
+
pip install -r requirements.txt -U
|
| 35 |
+
```
|
| 36 |
+
|
| 37 |
+
## Development
|
| 38 |
+
|
| 39 |
+
```bash
|
| 40 |
+
# Start server on localhost:8080
|
| 41 |
+
|
| 42 |
+
lep photon run -n tryemoji -m photon/main.py --local
|
| 43 |
+
```
|
| 44 |
+
|
| 45 |
+
```bash
|
| 46 |
+
# Start web server on localhost:3000
|
| 47 |
+
|
| 48 |
+
npm run dev
|
| 49 |
+
```
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
## Built with
|
| 54 |
+
|
| 55 |
+
- [Lepton AI](https://github.com/leptonai/leptonai)
|
| 56 |
+
- [emoji-mart](https://github.com/missive/emoji-mart)
|
| 57 |
+
- [Real-Time-Latent-Consistency-Model](https://huggingface.co/spaces/radames/Real-Time-Latent-Consistency-Model)
|
index.html
CHANGED
|
@@ -3,17 +3,10 @@
|
|
| 3 |
<head>
|
| 4 |
<meta charset="utf-8" />
|
| 5 |
<meta name="viewport" content="width=device-width" />
|
| 6 |
-
<title
|
| 7 |
<link rel="stylesheet" href="style.css" />
|
| 8 |
</head>
|
| 9 |
<body>
|
| 10 |
-
|
| 11 |
-
<h1>Welcome to your static Space!</h1>
|
| 12 |
-
<p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
|
| 13 |
-
<p>
|
| 14 |
-
Also don't forget to check the
|
| 15 |
-
<a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
|
| 16 |
-
</p>
|
| 17 |
-
</div>
|
| 18 |
</body>
|
| 19 |
</html>
|
|
|
|
| 3 |
<head>
|
| 4 |
<meta charset="utf-8" />
|
| 5 |
<meta name="viewport" content="width=device-width" />
|
| 6 |
+
<title>🐤 tryEmoji</title>
|
| 7 |
<link rel="stylesheet" href="style.css" />
|
| 8 |
</head>
|
| 9 |
<body>
|
| 10 |
+
<iframe src="https://www.tryemoji.com/"></iframe>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
</body>
|
| 12 |
</html>
|
next.config.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/** @type {import('next').NextConfig} */
|
| 2 |
+
const nextConfig = {};
|
| 3 |
+
|
| 4 |
+
module.exports = nextConfig;
|
package-lock.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "useemoji",
|
| 3 |
+
"version": "0.1.0",
|
| 4 |
+
"private": true,
|
| 5 |
+
"scripts": {
|
| 6 |
+
"dev": "next dev",
|
| 7 |
+
"build": "next build",
|
| 8 |
+
"start": "next start",
|
| 9 |
+
"lint": "next lint"
|
| 10 |
+
},
|
| 11 |
+
"dependencies": {
|
| 12 |
+
"@emoji-mart/data": "^1.1.2",
|
| 13 |
+
"@emoji-mart/react": "^1.1.1",
|
| 14 |
+
"@radix-ui/react-select": "^2.0.0",
|
| 15 |
+
"@radix-ui/react-slider": "^1.1.2",
|
| 16 |
+
"@radix-ui/react-toast": "^1.1.5",
|
| 17 |
+
"@radix-ui/react-tooltip": "^1.0.7",
|
| 18 |
+
"@vercel/analytics": "^1.1.1",
|
| 19 |
+
"@vercel/kv": "^1.0.0",
|
| 20 |
+
"class-variance-authority": "^0.7.0",
|
| 21 |
+
"clsx": "^2.0.0",
|
| 22 |
+
"emoji-mart": "^5.5.2",
|
| 23 |
+
"lucide-react": "^0.294.0",
|
| 24 |
+
"lz-string": "^1.5.0",
|
| 25 |
+
"next": "14.0.3",
|
| 26 |
+
"react": "^18",
|
| 27 |
+
"react-dom": "^18",
|
| 28 |
+
"react-responsive": "^9.0.2",
|
| 29 |
+
"react-share": "^5.0.2",
|
| 30 |
+
"swr": "^2.2.4",
|
| 31 |
+
"tailwind-merge": "^2.0.0",
|
| 32 |
+
"use-debounce": "^10.0.0"
|
| 33 |
+
},
|
| 34 |
+
"devDependencies": {
|
| 35 |
+
"@types/lz-string": "^1.5.0",
|
| 36 |
+
"@types/node": "^20",
|
| 37 |
+
"@types/react": "^18",
|
| 38 |
+
"@types/react-dom": "^18",
|
| 39 |
+
"autoprefixer": "^10.0.1",
|
| 40 |
+
"eslint": "^8",
|
| 41 |
+
"eslint-config-next": "14.0.3",
|
| 42 |
+
"eslint-config-prettier": "^9.0.0",
|
| 43 |
+
"eslint-plugin-prettier": "^5.0.1",
|
| 44 |
+
"eslint-plugin-unused-imports": "^3.0.0",
|
| 45 |
+
"postcss": "^8",
|
| 46 |
+
"prettier": "^3.1.0",
|
| 47 |
+
"tailwindcss": "^3.3.0",
|
| 48 |
+
"typescript": "^5"
|
| 49 |
+
}
|
| 50 |
+
}
|
photon/main.py
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from contextlib import nullcontext
|
| 2 |
+
from io import BytesIO
|
| 3 |
+
import os
|
| 4 |
+
import threading
|
| 5 |
+
from typing import Optional, Union
|
| 6 |
+
import warnings
|
| 7 |
+
|
| 8 |
+
from compel import Compel
|
| 9 |
+
from fastapi.responses import StreamingResponse
|
| 10 |
+
from loguru import logger
|
| 11 |
+
from PIL import Image
|
| 12 |
+
import torch
|
| 13 |
+
|
| 14 |
+
from leptonai.photon import Photon, FileParam, get_file_content, HTTPException
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
EXAMPLE_IMAGE_BASE64 = "/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAkGBxAQEBANDxIQEA8PDw8PDxUPEg8NDxUPFRIWFhURFRYYHSggGBolGxUVITEhJSkrLi4uFx8zODMsNygtLisBCgoKDg0OGBAQFysfHx8tKy4tKy0tKystLS0rKy0tLSstNy4tLy0tLS0tKy0tLSsrLS0rLS0tLS0tLS0rKzctK//AABEIAOEA4QMBEQACEQEDEQH/xAAbAAEAAgMBAQAAAAAAAAAAAAAAAQMCBAYHBf/EAEAQAQACAQIBCAUIBwkBAAAAAAABAgMEETEFBhIhQXGRoVFhgbHBBxMyQ1JyktEVIkJic4LhJFNjk6KywuLwFP/EABoBAQEAAwEBAAAAAAAAAAAAAAABAgMFBAb/xAAtEQEAAgIBAgMIAQUBAAAAAAAAAQIDEQQSUSFBkQUTIjFCUmFxMiMzgaHBFP/aAAwDAQACEQMRAD8A9uBIJBIAAAAAAAAAAAAAAAAAAAAAAAAAMAZQACQAAAAAAAAAAAAAAAAAAAAAAAAAYgmASAAAAAAAAAAAAAAAAAAAAAAAAAACASAAAAAAAAAAAAAAAAAAAAAAAAAAACIBIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAI3BIAAAI3BIAAAAAAAAAAAAAAAAAAOd5b546XSV6V5m28zEdHqrMx2RPb7Gi2esfLxerHxL2+fg4/V/K12YcEd95mfyaLcq3lD2U9n087S+ff5S9bb6PzdO6sT792qeTk7t9eBh7f7Uzz311/rZjuise6Guc2T7m6OHhj6YTHOXWW458vstaPixnJefOfVn/AObFH0x6M45Z1E8cuSe+1p+KdVu7L3NPtj0ZRypm/vLeMnVPc91TtHon9KZvt28U6p7r7qnZH6YzxwvbxlOue6+5p2j0ZRzg1NeGXJ+O8fFfeWjzljPHxT9Mei7Hzy1dP25mPXMW98M45GSPNrtwsM/S+7yTz1yXibZOj0YjebXjoV7otHVu9FOTfzeLLwMcfKdO20Oqrmx0zV+jkrFo7pe+s7jblXr02mOy9WIAAAAAAAAAAAAAADxzVx0t8d60yUi07VyVi8Rt2xvwn1uRaZiZ0+kpWJiNvnX5E00/VTTf7F7x5WmYhh1tnR+WMc3sP7Ns0d847f8AGE6mWpW15CrHDJf21ifieC7lbXkiI+sn8H/Y8DcrY0ER+3P4P6htP/yx9qfCPzDaJwR+95QmoNyqtjr6J9sx+RqF3Km+32Y9s3/M0m5a+TLaOG0d1axPjtuyhjLXyTaZibTNp/emZlshqs9t5qzvotN/Bp7nTx/xhwM/9y37fVZtQAAAAAAAAAAAAAADyLW02y5I9GS8eFpcjJHjL6TDO6R+mFatTcsrHqFZxHqETt6lETt6BFdkVXZRReBWvkgRq3hlEMZa8x1s4arPbeasf2LS/wAGnudPH/GHBz/3Lft9Vm1AAAAAAAAAAAAAAAPKeWqdHU56+jNkn2TaZj3uVljV5fQ8ad46/pr0lpelbWUGSiYkRjaQVyiqrqKbg1sqjVuyhjKiI62cNcvcuQMfQ0umrPGMGLfv6EbunSNVh89knd5n8t9kwAAAAAAAAAAAAAAAcZzv5vTM31uOY22i2Ws9XCIjpVnu26p9c7vJnwb+KHR4fK6dY7f4ch1xxie/jHjweGay68XiWdLwx0yWxYDcETIMJkFdpXRtr3tBo2pms24RM90TK9MsZlVOnt27V75jfw4s4rLGbQ6fmLzew6i98mXpXrh6G0fRpa1t+qe2Yjbh1cXqwY4nxlzuZntTVa+b02IexykgAAAAAAAiASAAAAAACrV4IyY74p4Xpak91omPikxuNLWdTEvGsmG0TtO8THVPfDl2nT6KmpjwZRW/pme/rY9TZ0soi3q8ITa6Zb29Eea7TUotafRH+r802uvywm0+rzNmmFrT6vwx8V2aYTktHbt3bR7jZ0qMl7TxmZ79zadMKpiZ4yyiWMxp6f8AJzp+jpLX7cma0x3RER74l78EfC4vNtvJrs6tueQAAAAAAABiCYBIAAAAAAPMucOl6GqzV7JvN47r/rfFzc0avLu8S3Viq0a1aHsZxRA6AMJoKwnGIptSFVTesKjXuqSq7WUMLPYeamDoaLT19OOL/jmbfF0scarD5/PbqyWl9Zm1AAAAAAAAMATAJBIAAAAAOJ584Ns2PJ9vH0fbWfytHg8XKr4xLq+z7fDMdnOQ8bpwziUVEyCJBhbYFGSYUa95Ua2SVSWOKs2tFY4zMRHfPVDOsbnTVedRMvccGKKUrSOFK1rHdEbOpD52Z3O1ggAAAAAAADCATAJgEgAAAAA57ntp+lp63jjjyRv923V7+i8/Irum3s4NtZNd3DOdLt1TFkZG67XTGZQYWlYFNga2SVGveVhjLf5sYPnNZp6f4tbT3V/Wnyq3Yo3aHk5VunHZ7M6LhAAAAAAAAAMIBIJgEgAAAAA0+WNP85gy4+2cduj96OuvnEMbxusw2YrdN4l5jXrcqYfRVkmrBmgUkFdwUXkhWtkVGteWUMZdL8nmKJ1nTnhixXt7Z2r7rS9XGj4nO59v6eu8vUIyw9rkMotAJAAAAAAABWCYkE7gkEgAAAAiQeW8paf5rPlx8IrktEfd36vLZy8katMPoePbqpWVUW9TU3onZGSJQV2lRr5BWtkWEa2RnDCXU8xabRmyemaUj2bzPvh7ONHzlyudPjEOux5p9L1Q5+mzj1NlRtY9QiNmmUFkWBIAAAAKwSCQSCQAAAAAcFz20/R1EZNurLSJ/mr+rPlFXh5Nfi33dj2ffdNdnway8bpMoBEgrsCi6jVyKNXIyhhLtuaeLo6as/bte8+O0eVYdDDGquLyrbyT+H3sdW6HlbGOqo2cdRi2KQguqC2ASAAADAE7AAkEgAAAAA5vnxpelgrljjiv1/dt1T5xVo5Fd132e3g36cmu7hYlzZd2GcSiomQYWkGvlso1MsqjTy2/ozrG2q9tRt6byLp9sWOkcK0rXwjrdSldQ4OS27TL7OLTMmqZbNNObYr64kGcUBnFQZAAAAAxAgEgkAAAAAAFWow1vW1LxFq2ia2ie2JNbWJ1O4cbr+Zt95nT5KzHZTNvWY/nrE7+2Pa8t+LE/wAZ06eL2hMeF42+Vl5B1dOOC0x6aTTJ5RO/k888XJD1152GfPTUyaPPHHBqP8jNMf7WucN/tlujkYZ+uPVRbT5uzDqJ7sGaZ8qnub/bK+/xffHqxryVq7/R02o/mxXx+d4iGcYMk+TXPMwx9X/V+Hmdyhk+rx4fXmy14emIx9Lw6m2vFt5vPf2hjj5RMvucj/J5Sloy6nLOe9Z3ita/N4Yn09HeZn2z7IevHhpTxeDLy75PDydpg0laxtENm3l2vikIidgSAAAAAACN/wD3UBsCQAAAAAAAAQCJgU2U2bAAAbAmIREgAAAAAAAAAx6/V4gyAAAAAAAAAAAABGwGwGwJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB/9k="
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class JPEGResponse(StreamingResponse):
|
| 21 |
+
media_type = "image/jpeg"
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class ImgPilot(Photon):
|
| 25 |
+
requirement_dependency = [
|
| 26 |
+
"torch",
|
| 27 |
+
"diffusers",
|
| 28 |
+
"invisible-watermark",
|
| 29 |
+
"compel",
|
| 30 |
+
"Pillow",
|
| 31 |
+
]
|
| 32 |
+
|
| 33 |
+
# In default, we will use gpu.a10 as the computation resource shape. This should
|
| 34 |
+
# be fast enough.
|
| 35 |
+
deployment_template = {
|
| 36 |
+
"resource_shape": "gpu.a10",
|
| 37 |
+
"env": {
|
| 38 |
+
"MODEL": "SimianLuo/LCM_Dreamshaper_v7",
|
| 39 |
+
"USE_TORCH_COMPILE": "false",
|
| 40 |
+
"WIDTH": "768",
|
| 41 |
+
"HEIGHT": "768",
|
| 42 |
+
"PRINT_PROMPT": "false",
|
| 43 |
+
},
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
# A10 should be able to support a maximum concurrency of 8 requests to interleave
|
| 47 |
+
# IO and compute. This is not tuned by the way.
|
| 48 |
+
handler_max_concurrency = 1
|
| 49 |
+
|
| 50 |
+
def init(self):
|
| 51 |
+
from diffusers import AutoPipelineForImage2Image # type: ignore
|
| 52 |
+
|
| 53 |
+
cuda_available = torch.cuda.is_available()
|
| 54 |
+
|
| 55 |
+
if cuda_available:
|
| 56 |
+
self.device = torch.device("cuda")
|
| 57 |
+
else:
|
| 58 |
+
self.device = torch.device("cpu")
|
| 59 |
+
|
| 60 |
+
self.base = AutoPipelineForImage2Image.from_pretrained(
|
| 61 |
+
os.environ["MODEL"],
|
| 62 |
+
torch_dtype=torch.float16 if cuda_available else torch.float32,
|
| 63 |
+
)
|
| 64 |
+
self.base.safety_checker = None
|
| 65 |
+
self.base.requires_safety_checker = False
|
| 66 |
+
if self.handler_max_concurrency > 1:
|
| 67 |
+
self.base_lock = threading.Lock()
|
| 68 |
+
else:
|
| 69 |
+
self.base_lock = nullcontext()
|
| 70 |
+
self.print_prompt = os.environ["PRINT_PROMPT"].lower() in [
|
| 71 |
+
"true",
|
| 72 |
+
"t",
|
| 73 |
+
"1",
|
| 74 |
+
"yes",
|
| 75 |
+
"y",
|
| 76 |
+
]
|
| 77 |
+
logger.info(f"print_prompt: {self.print_prompt}")
|
| 78 |
+
if cuda_available:
|
| 79 |
+
self.base.to("cuda")
|
| 80 |
+
self.use_torch_compile = os.environ["USE_TORCH_COMPILE"].lower() in [
|
| 81 |
+
"true",
|
| 82 |
+
"t",
|
| 83 |
+
"1",
|
| 84 |
+
"yes",
|
| 85 |
+
"y",
|
| 86 |
+
]
|
| 87 |
+
if self.use_torch_compile:
|
| 88 |
+
if self.handler_max_concurrency > 1:
|
| 89 |
+
warnings.warn(
|
| 90 |
+
"torch compile does not support multithreading, so we will"
|
| 91 |
+
" disable torch compile since handler_max_concurrency > 1."
|
| 92 |
+
)
|
| 93 |
+
else:
|
| 94 |
+
self.width = int(os.environ["WIDTH"])
|
| 95 |
+
self.height = int(os.environ["HEIGHT"])
|
| 96 |
+
logger.info(
|
| 97 |
+
"Compiling model with torch.compile. Note that with torch"
|
| 98 |
+
" compile, your first invocation will be slow, but subsequent"
|
| 99 |
+
" invocations will be faster."
|
| 100 |
+
)
|
| 101 |
+
self.base.unet = torch.compile(
|
| 102 |
+
self.base.unet, mode="reduce-overhead", fullgraph=True
|
| 103 |
+
)
|
| 104 |
+
else:
|
| 105 |
+
self.use_torch_compile = False
|
| 106 |
+
|
| 107 |
+
self.compel_proc = Compel(
|
| 108 |
+
tokenizer=self.base.tokenizer,
|
| 109 |
+
text_encoder=self.base.text_encoder,
|
| 110 |
+
truncate_long_prompts=False,
|
| 111 |
+
) # type: ignore
|
| 112 |
+
|
| 113 |
+
logger.info(f"Initialized model {os.environ['MODEL']}. cuda: {cuda_available}.")
|
| 114 |
+
|
| 115 |
+
@Photon.handler(
|
| 116 |
+
"run",
|
| 117 |
+
example={
|
| 118 |
+
"prompt": (
|
| 119 |
+
"Portrait of The Terminator, glare pose, detailed, intricate, full of"
|
| 120 |
+
" colour, cinematic lighting, trending on artstation, 8k,"
|
| 121 |
+
" hyperrealistic, focused, extreme details, unreal engine 5, cinematic,"
|
| 122 |
+
" masterpiece"
|
| 123 |
+
),
|
| 124 |
+
"seed": 2159232,
|
| 125 |
+
"strength": 0.5,
|
| 126 |
+
"steps": 4,
|
| 127 |
+
"guidance_scale": 8.0,
|
| 128 |
+
"width": 512,
|
| 129 |
+
"height": 512,
|
| 130 |
+
"lcm_steps": 50,
|
| 131 |
+
"input_image": EXAMPLE_IMAGE_BASE64,
|
| 132 |
+
},
|
| 133 |
+
)
|
| 134 |
+
def run(
|
| 135 |
+
self,
|
| 136 |
+
prompt: str,
|
| 137 |
+
seed: int,
|
| 138 |
+
strength: float,
|
| 139 |
+
steps: int,
|
| 140 |
+
guidance_scale: float,
|
| 141 |
+
width: int,
|
| 142 |
+
height: int,
|
| 143 |
+
lcm_steps: int,
|
| 144 |
+
input_image: Optional[Union[str, FileParam]],
|
| 145 |
+
) -> JPEGResponse:
|
| 146 |
+
from diffusers.utils import load_image # type: ignore
|
| 147 |
+
import time
|
| 148 |
+
|
| 149 |
+
start = time.time()
|
| 150 |
+
|
| 151 |
+
if self.print_prompt:
|
| 152 |
+
logger.info(f"Prompt: {prompt}")
|
| 153 |
+
|
| 154 |
+
# diffusers truncates prompt to 77 tokens, in case prompt is too long, we will
|
| 155 |
+
# use compel to process the prompt (but compel is slower)
|
| 156 |
+
tokens = self.base.tokenizer(prompt, return_tensors="pt")
|
| 157 |
+
if tokens.input_ids.shape[1] > 77:
|
| 158 |
+
prompt_embeds = self.compel_proc(prompt)
|
| 159 |
+
prompt = None
|
| 160 |
+
else:
|
| 161 |
+
prompt_embeds = None
|
| 162 |
+
|
| 163 |
+
if input_image is not None:
|
| 164 |
+
image_file = get_file_content(input_image, return_file=True)
|
| 165 |
+
pil_image = Image.open(image_file, formats=["JPEG", "PNG", "GIF", "BMP"])
|
| 166 |
+
if self.use_torch_compile:
|
| 167 |
+
# checks width and height parameter, and return error if width and height are not correct
|
| 168 |
+
if width != self.width or height != self.height:
|
| 169 |
+
raise HTTPException(
|
| 170 |
+
status_code=400,
|
| 171 |
+
detail=(
|
| 172 |
+
f"width and height must be {self.width} and"
|
| 173 |
+
f" {self.height} when use_torch_compile is true."
|
| 174 |
+
),
|
| 175 |
+
)
|
| 176 |
+
# checks input image height and width, and resize if necessary
|
| 177 |
+
if pil_image.height != self.height or pil_image.width != self.width:
|
| 178 |
+
pil_image = pil_image.resize(
|
| 179 |
+
(self.width, self.height), Image.BILINEAR
|
| 180 |
+
)
|
| 181 |
+
input_image = load_image(pil_image).convert("RGB")
|
| 182 |
+
|
| 183 |
+
with self.base_lock:
|
| 184 |
+
generator = torch.manual_seed(seed)
|
| 185 |
+
output_image = self.base(
|
| 186 |
+
prompt=prompt,
|
| 187 |
+
prompt_embeds=prompt_embeds,
|
| 188 |
+
generator=generator,
|
| 189 |
+
image=input_image,
|
| 190 |
+
strength=strength,
|
| 191 |
+
num_inference_steps=steps,
|
| 192 |
+
guidance_scale=guidance_scale,
|
| 193 |
+
width=width,
|
| 194 |
+
height=height,
|
| 195 |
+
lcm_origin_steps=lcm_steps,
|
| 196 |
+
output_type="pil",
|
| 197 |
+
) # type: ignore
|
| 198 |
+
|
| 199 |
+
nsfw_content_detected = (
|
| 200 |
+
output_image.nsfw_content_detected[0]
|
| 201 |
+
if "nsfw_content_detected" in output_image
|
| 202 |
+
else False
|
| 203 |
+
) # type: ignore
|
| 204 |
+
if nsfw_content_detected:
|
| 205 |
+
raise HTTPException(status_code=400, detail="nsfw content detected")
|
| 206 |
+
else:
|
| 207 |
+
img_io = BytesIO()
|
| 208 |
+
output_image.images[0].save(img_io, format="JPEG") # type: ignore
|
| 209 |
+
img_io.seek(0)
|
| 210 |
+
logger.info(f"Produced output in {time.time() - start} seconds.")
|
| 211 |
+
return JPEGResponse(img_io)
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
if __name__ == "__main__":
|
| 215 |
+
p = ImgPilot()
|
| 216 |
+
p.launch()
|
postcss.config.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
module.exports = {
|
| 2 |
+
plugins: {
|
| 3 |
+
tailwindcss: {},
|
| 4 |
+
autoprefixer: {},
|
| 5 |
+
},
|
| 6 |
+
}
|
requirements.txt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
leptonai
|
| 2 |
+
torch
|
| 3 |
+
diffusers
|
| 4 |
+
invisible-watermark
|
| 5 |
+
compel
|
| 6 |
+
Pillow
|
src/app/api/run/route.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextRequest } from "next/server";
|
| 2 |
+
|
| 3 |
+
const API_URL = process.env?.API_URL || "http://127.0.0.1:8080";
|
| 4 |
+
const API_TOKEN = process.env?.API_TOKEN || "";
|
| 5 |
+
|
| 6 |
+
export async function POST(req: NextRequest) {
|
| 7 |
+
const headers = new Headers();
|
| 8 |
+
headers.set("Accept", `image/jpeg`);
|
| 9 |
+
headers.set("Authorization", `Bearer ${API_TOKEN}`);
|
| 10 |
+
headers.set(
|
| 11 |
+
"Content-Type",
|
| 12 |
+
req.headers.get("Content-Type") || "application/json",
|
| 13 |
+
);
|
| 14 |
+
const url = new URL("/run", API_URL);
|
| 15 |
+
|
| 16 |
+
return fetch(url.toString(), {
|
| 17 |
+
body: req.body,
|
| 18 |
+
method: req.method,
|
| 19 |
+
headers,
|
| 20 |
+
duplex: "half",
|
| 21 |
+
} as unknown as RequestInit);
|
| 22 |
+
}
|
src/app/api/share/route.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextRequest } from "next/server";
|
| 2 |
+
import { createClient } from "@vercel/kv";
|
| 3 |
+
|
| 4 |
+
const kv =
|
| 5 |
+
process.env?.KV_REST_API_URL && process.env?.KV_REST_API_TOKEN
|
| 6 |
+
? createClient({
|
| 7 |
+
url: process.env.KV_REST_API_URL,
|
| 8 |
+
token: process.env.KV_REST_API_TOKEN,
|
| 9 |
+
})
|
| 10 |
+
: null;
|
| 11 |
+
|
| 12 |
+
export async function POST(req: NextRequest) {
|
| 13 |
+
const { key, image } = await req.json();
|
| 14 |
+
|
| 15 |
+
if (!kv || !key || !image) {
|
| 16 |
+
return new Response("", {
|
| 17 |
+
status: 200,
|
| 18 |
+
});
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
const slug = key.replace(/[^a-zA-Z0-9]/g, "_");
|
| 22 |
+
|
| 23 |
+
await kv.set(slug, image);
|
| 24 |
+
|
| 25 |
+
return new Response("", {
|
| 26 |
+
status: 200,
|
| 27 |
+
});
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
export async function GET(req: NextRequest) {
|
| 31 |
+
const key = req.nextUrl.searchParams.get("share");
|
| 32 |
+
|
| 33 |
+
if (!kv || !key) {
|
| 34 |
+
return new Response("", {
|
| 35 |
+
status: 200,
|
| 36 |
+
});
|
| 37 |
+
}
|
| 38 |
+
const slug = key.replace(/[^a-zA-Z0-9]/g, "_");
|
| 39 |
+
const image = await kv.get<string>(slug);
|
| 40 |
+
|
| 41 |
+
if (!image) {
|
| 42 |
+
return new Response("", {
|
| 43 |
+
status: 200,
|
| 44 |
+
});
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
return new Response(image, {
|
| 48 |
+
status: 200,
|
| 49 |
+
});
|
| 50 |
+
}
|
src/app/globals.css
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@tailwind base;
|
| 2 |
+
@tailwind components;
|
| 3 |
+
@tailwind utilities;
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
[data-unified="1f5ff"],
|
| 7 |
+
[data-unified="1f997"],
|
| 8 |
+
[data-unified="1fab1"],
|
| 9 |
+
[data-unified="1f95c"],
|
| 10 |
+
[data-unified="1fa7b"],
|
| 11 |
+
[data-unified="1fa79"] {
|
| 12 |
+
display: none !important;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
em-emoji-picker {
|
| 17 |
+
height: 512px;
|
| 18 |
+
width: 320px;
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
@media (max-width: 768px) {
|
| 22 |
+
em-emoji-picker {
|
| 23 |
+
height: 100px;
|
| 24 |
+
min-height: 100px;
|
| 25 |
+
width: 100%;
|
| 26 |
+
}
|
| 27 |
+
}
|
src/app/icon.svg
ADDED
|
|
src/app/layout.tsx
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { Metadata, Viewport } from "next";
|
| 2 |
+
import { Sriracha } from "next/font/google";
|
| 3 |
+
import "./globals.css";
|
| 4 |
+
import { ReactNode } from "react";
|
| 5 |
+
import { Analytics } from "@vercel/analytics/react";
|
| 6 |
+
|
| 7 |
+
const inter = Sriracha({ weight: "400", subsets: ["latin"] });
|
| 8 |
+
|
| 9 |
+
const title = "tryEmoji";
|
| 10 |
+
const description = "Turn emoji into amazing artwork via AI";
|
| 11 |
+
|
| 12 |
+
export const metadata: Metadata = {
|
| 13 |
+
title,
|
| 14 |
+
description,
|
| 15 |
+
openGraph: {
|
| 16 |
+
title,
|
| 17 |
+
description,
|
| 18 |
+
type: "website",
|
| 19 |
+
url: "https://tryemoji.com",
|
| 20 |
+
images: [
|
| 21 |
+
{
|
| 22 |
+
url: "https://tryemoji.com/og.png",
|
| 23 |
+
width: 630,
|
| 24 |
+
height: 473,
|
| 25 |
+
alt: "tryEmoji",
|
| 26 |
+
},
|
| 27 |
+
],
|
| 28 |
+
},
|
| 29 |
+
};
|
| 30 |
+
|
| 31 |
+
const viewport: Viewport = {
|
| 32 |
+
width: "device-width",
|
| 33 |
+
initialScale: 1,
|
| 34 |
+
userScalable: false,
|
| 35 |
+
maximumScale: 1,
|
| 36 |
+
minimumScale: 1,
|
| 37 |
+
};
|
| 38 |
+
|
| 39 |
+
export { viewport };
|
| 40 |
+
|
| 41 |
+
export default function RootLayout({ children }: { children: ReactNode }) {
|
| 42 |
+
return (
|
| 43 |
+
<html lang="en">
|
| 44 |
+
<body className={inter.className}>{children}</body>
|
| 45 |
+
<Analytics></Analytics>
|
| 46 |
+
</html>
|
| 47 |
+
);
|
| 48 |
+
}
|
src/app/og/image.png
ADDED
|
src/app/og/route.tsx
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { ImageResponse } from "next/og";
|
| 2 |
+
import { createClient } from "@vercel/kv";
|
| 3 |
+
import { shareString2Json } from "@/util/use-share";
|
| 4 |
+
|
| 5 |
+
const kv =
|
| 6 |
+
process.env?.KV_REST_API_URL && process.env?.KV_REST_API_TOKEN
|
| 7 |
+
? createClient({
|
| 8 |
+
url: process.env.KV_REST_API_URL,
|
| 9 |
+
token: process.env.KV_REST_API_TOKEN,
|
| 10 |
+
})
|
| 11 |
+
: null;
|
| 12 |
+
|
| 13 |
+
export const runtime = "edge";
|
| 14 |
+
|
| 15 |
+
const siteUrl =
|
| 16 |
+
process.env.NODE_ENV === "production"
|
| 17 |
+
? "https://www.tryemoji.com/"
|
| 18 |
+
: "http://localhost:3000/";
|
| 19 |
+
|
| 20 |
+
export async function GET(request: Request) {
|
| 21 |
+
const { searchParams } = new URL(request.url);
|
| 22 |
+
const image = await fetch(new URL("./image.png", import.meta.url)).then(
|
| 23 |
+
(res) => res.arrayBuffer(),
|
| 24 |
+
);
|
| 25 |
+
let base64URL = "";
|
| 26 |
+
let emoji = "";
|
| 27 |
+
const share = searchParams.get("share");
|
| 28 |
+
const option = share ? shareString2Json(share as string) : null;
|
| 29 |
+
const apiURL = new URL("api/share", siteUrl);
|
| 30 |
+
if (share) {
|
| 31 |
+
apiURL.searchParams.set("share", share);
|
| 32 |
+
const res = await fetch(apiURL.toString());
|
| 33 |
+
base64URL = await res.text();
|
| 34 |
+
emoji = option?.emoji || "👍";
|
| 35 |
+
}
|
| 36 |
+
return new ImageResponse(
|
| 37 |
+
(
|
| 38 |
+
<div
|
| 39 |
+
style={{
|
| 40 |
+
display: "flex",
|
| 41 |
+
background: "#f6f6f6",
|
| 42 |
+
width: "100%",
|
| 43 |
+
height: "100%",
|
| 44 |
+
flexDirection: "column",
|
| 45 |
+
justifyContent: "center",
|
| 46 |
+
alignItems: "center",
|
| 47 |
+
position: "relative",
|
| 48 |
+
}}
|
| 49 |
+
>
|
| 50 |
+
<img
|
| 51 |
+
style={{
|
| 52 |
+
position: "absolute",
|
| 53 |
+
top: 0,
|
| 54 |
+
left: 0,
|
| 55 |
+
width: "100%",
|
| 56 |
+
height: "100%",
|
| 57 |
+
objectFit: "cover",
|
| 58 |
+
objectPosition: "center",
|
| 59 |
+
}}
|
| 60 |
+
width="800"
|
| 61 |
+
height="471"
|
| 62 |
+
src={image as unknown as string}
|
| 63 |
+
/>
|
| 64 |
+
{emoji && (
|
| 65 |
+
<span
|
| 66 |
+
style={{
|
| 67 |
+
width: 45,
|
| 68 |
+
height: 45,
|
| 69 |
+
textAlign: "center",
|
| 70 |
+
lineHeight: "40px",
|
| 71 |
+
background: "#0a0a0b",
|
| 72 |
+
position: "absolute",
|
| 73 |
+
top: 38,
|
| 74 |
+
left: 305,
|
| 75 |
+
fontSize: 40,
|
| 76 |
+
fontFamily: "sans-serif",
|
| 77 |
+
}}
|
| 78 |
+
>
|
| 79 |
+
{emoji}
|
| 80 |
+
</span>
|
| 81 |
+
)}
|
| 82 |
+
{base64URL && (
|
| 83 |
+
<img
|
| 84 |
+
src={base64URL}
|
| 85 |
+
width="302"
|
| 86 |
+
height="302"
|
| 87 |
+
style={{
|
| 88 |
+
position: "absolute",
|
| 89 |
+
borderRadius: 8,
|
| 90 |
+
top: 125,
|
| 91 |
+
right: 150,
|
| 92 |
+
width: 302,
|
| 93 |
+
height: 302,
|
| 94 |
+
objectFit: "cover",
|
| 95 |
+
objectPosition: "center",
|
| 96 |
+
}}
|
| 97 |
+
/>
|
| 98 |
+
)}
|
| 99 |
+
</div>
|
| 100 |
+
),
|
| 101 |
+
{
|
| 102 |
+
width: 800,
|
| 103 |
+
height: 471,
|
| 104 |
+
},
|
| 105 |
+
);
|
| 106 |
+
}
|
src/app/page.tsx
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Metadata } from "next";
|
| 2 |
+
import TryEmoji from "@/components/try-emoji";
|
| 3 |
+
|
| 4 |
+
type Props = {
|
| 5 |
+
params: { share?: string };
|
| 6 |
+
searchParams: { [key: string]: string | string[] | undefined };
|
| 7 |
+
};
|
| 8 |
+
|
| 9 |
+
export async function generateMetadata(po: Props): Promise<Metadata> {
|
| 10 |
+
// read route params
|
| 11 |
+
const share = po.searchParams?.share;
|
| 12 |
+
|
| 13 |
+
const siteUrl =
|
| 14 |
+
process.env.NODE_ENV === "production"
|
| 15 |
+
? "https://www.tryemoji.com/"
|
| 16 |
+
: "http://localhost:3000/";
|
| 17 |
+
const ogUrl = new URL("og", siteUrl);
|
| 18 |
+
if (share) {
|
| 19 |
+
ogUrl.searchParams.set("share", share as string);
|
| 20 |
+
}
|
| 21 |
+
return {
|
| 22 |
+
openGraph: {
|
| 23 |
+
images: [
|
| 24 |
+
{
|
| 25 |
+
url: ogUrl.toString(),
|
| 26 |
+
width: 630,
|
| 27 |
+
height: 473,
|
| 28 |
+
alt: "tryEmoji",
|
| 29 |
+
},
|
| 30 |
+
],
|
| 31 |
+
},
|
| 32 |
+
};
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
export default function Home() {
|
| 36 |
+
return <TryEmoji />;
|
| 37 |
+
}
|
src/components/dice.tsx
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Dice1, Dice2, Dice3, Dice4, Dice5, Dice6 } from "lucide-react";
|
| 2 |
+
import { FC, useState } from "react";
|
| 3 |
+
|
| 4 |
+
const dices = [
|
| 5 |
+
<Dice1 key="dice-1" />,
|
| 6 |
+
<Dice2 key="dice-2" />,
|
| 7 |
+
<Dice3 key="dice-3" />,
|
| 8 |
+
<Dice4 key="dice-4" />,
|
| 9 |
+
<Dice5 key="dice-5" />,
|
| 10 |
+
<Dice6 key="dice-6" />,
|
| 11 |
+
];
|
| 12 |
+
|
| 13 |
+
export const Dice: FC = () => {
|
| 14 |
+
const [click, setClick] = useState(false);
|
| 15 |
+
const [currentNumber, setCurrentNumber] = useState(3);
|
| 16 |
+
const rollDice = () => {
|
| 17 |
+
let rollingTime = 0;
|
| 18 |
+
setClick(true);
|
| 19 |
+
|
| 20 |
+
const rollInterval = setInterval(
|
| 21 |
+
() => {
|
| 22 |
+
setCurrentNumber(Math.floor(Math.random() * 6));
|
| 23 |
+
rollingTime += 100;
|
| 24 |
+
// Slow down the rolling
|
| 25 |
+
if (rollingTime >= 1200) {
|
| 26 |
+
clearInterval(rollInterval);
|
| 27 |
+
setClick(false);
|
| 28 |
+
}
|
| 29 |
+
},
|
| 30 |
+
100 - rollingTime / 20,
|
| 31 |
+
);
|
| 32 |
+
};
|
| 33 |
+
return (
|
| 34 |
+
<div
|
| 35 |
+
className={click ? "animate-[shake_1.2s_ease-in-out]" : ""}
|
| 36 |
+
onClick={(event) => {
|
| 37 |
+
event.preventDefault();
|
| 38 |
+
rollDice();
|
| 39 |
+
}}
|
| 40 |
+
>
|
| 41 |
+
{dices[currentNumber]}
|
| 42 |
+
</div>
|
| 43 |
+
);
|
| 44 |
+
};
|
src/components/emoji-selector.tsx
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import Picker from "@emoji-mart/react";
|
| 2 |
+
import data from "@emoji-mart/data";
|
| 3 |
+
import { FC } from "react";
|
| 4 |
+
import { useMediaQuery } from "react-responsive";
|
| 5 |
+
|
| 6 |
+
const emojis = (data as unknown as any).emojis as { [key: string]: EmojiSkin };
|
| 7 |
+
export interface EmojiData {
|
| 8 |
+
id: string;
|
| 9 |
+
name: string;
|
| 10 |
+
native: string;
|
| 11 |
+
unified: string;
|
| 12 |
+
keywords: string[];
|
| 13 |
+
shortcodes: string;
|
| 14 |
+
skin: number;
|
| 15 |
+
aliases: string[];
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
const exceptEmojis = [
|
| 19 |
+
"bat",
|
| 20 |
+
"feet",
|
| 21 |
+
"coral",
|
| 22 |
+
"snail",
|
| 23 |
+
"bug",
|
| 24 |
+
"ant",
|
| 25 |
+
"bee",
|
| 26 |
+
"beetle",
|
| 27 |
+
"ladybug",
|
| 28 |
+
"cricket",
|
| 29 |
+
"cockroach",
|
| 30 |
+
"spider",
|
| 31 |
+
"scorpion",
|
| 32 |
+
"mosquito",
|
| 33 |
+
"fly",
|
| 34 |
+
"worm",
|
| 35 |
+
"microbe",
|
| 36 |
+
"gorilla",
|
| 37 |
+
"orangutan",
|
| 38 |
+
"tiger2",
|
| 39 |
+
"leopard",
|
| 40 |
+
"zebra_face",
|
| 41 |
+
"pig_nose",
|
| 42 |
+
"camel",
|
| 43 |
+
"black_cat",
|
| 44 |
+
"water_buffalo",
|
| 45 |
+
"rat",
|
| 46 |
+
"spider_web",
|
| 47 |
+
"service_dog",
|
| 48 |
+
"mammoth",
|
| 49 |
+
"frog",
|
| 50 |
+
"crocodile",
|
| 51 |
+
"lizard",
|
| 52 |
+
"snake",
|
| 53 |
+
"t-rex",
|
| 54 |
+
"dragon",
|
| 55 |
+
"empty_nest",
|
| 56 |
+
"octopus",
|
| 57 |
+
"ox",
|
| 58 |
+
"wolf",
|
| 59 |
+
"headstone",
|
| 60 |
+
"moyai",
|
| 61 |
+
"new_moon",
|
| 62 |
+
"new_moon_with_face",
|
| 63 |
+
"shrimp",
|
| 64 |
+
"lobster",
|
| 65 |
+
"fried_shrimp",
|
| 66 |
+
"coffin",
|
| 67 |
+
"drop_of_blood",
|
| 68 |
+
"pinata",
|
| 69 |
+
"performing_arts",
|
| 70 |
+
"rock",
|
| 71 |
+
"clubs",
|
| 72 |
+
"chess_pawn",
|
| 73 |
+
"spades",
|
| 74 |
+
"knot",
|
| 75 |
+
"bathtub",
|
| 76 |
+
"shower",
|
| 77 |
+
"white_flower",
|
| 78 |
+
"hammer",
|
| 79 |
+
"nazar_amulet",
|
| 80 |
+
"hamsa",
|
| 81 |
+
"hammer_and_wrench",
|
| 82 |
+
"squid",
|
| 83 |
+
"crab",
|
| 84 |
+
"smoking",
|
| 85 |
+
"dna",
|
| 86 |
+
"musical_score",
|
| 87 |
+
"musical_note",
|
| 88 |
+
"notes",
|
| 89 |
+
"dark_sunglasses",
|
| 90 |
+
"kaaba",
|
| 91 |
+
"old_key",
|
| 92 |
+
"bikini",
|
| 93 |
+
"one-piece_swimsuit",
|
| 94 |
+
"sari",
|
| 95 |
+
"sloth",
|
| 96 |
+
"x-ray",
|
| 97 |
+
];
|
| 98 |
+
|
| 99 |
+
interface EmojiSkin {
|
| 100 |
+
id: string;
|
| 101 |
+
name: string;
|
| 102 |
+
keywords: string[];
|
| 103 |
+
skins: { native: string; shortcodes: string; unified: string }[];
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
const categoryIcons = {
|
| 107 |
+
categoryIcons: {
|
| 108 |
+
"new-people": {
|
| 109 |
+
svg: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path d="M57.89 397.2c-6.262-8.616-16.02-13.19-25.92-13.19c-23.33 0-31.98 20.68-31.98 32.03c0 6.522 1.987 13.1 6.115 18.78l46.52 64C58.89 507.4 68.64 512 78.55 512c23.29 0 31.97-20.66 31.97-32.03c0-6.522-1.988-13.1-6.115-18.78L57.89 397.2zM496.1 352c-44.13 0-79.72 35.75-79.72 80s35.59 80 79.72 80s79.91-35.75 79.91-80S540.2 352 496.1 352zM640 99.38c0-13.61-4.133-27.34-12.72-39.2l-23.63-32.5c-13.44-18.5-33.77-27.68-54.12-27.68c-13.89 0-27.79 4.281-39.51 12.8L307.8 159.7C262.2 192.8 220.4 230.9 183.4 273.4c-24.22 27.88-59.18 63.99-103.5 99.63l56.34 77.52c53.79-35.39 99.15-55.3 127.1-67.27c51.88-22 101.3-49.87 146.9-82.1l202.3-146.7C630.5 140.4 640 120 640 99.38z"/></svg>',
|
| 110 |
+
},
|
| 111 |
+
},
|
| 112 |
+
};
|
| 113 |
+
const custom = [
|
| 114 |
+
{
|
| 115 |
+
id: "recommend",
|
| 116 |
+
name: "Recommend",
|
| 117 |
+
emojis: [
|
| 118 |
+
emojis["baby_chick"],
|
| 119 |
+
emojis["hatched_chick"],
|
| 120 |
+
emojis["dog"],
|
| 121 |
+
emojis["fox_face"],
|
| 122 |
+
emojis["lion_face"],
|
| 123 |
+
emojis["tiger"],
|
| 124 |
+
emojis["hamster"],
|
| 125 |
+
emojis["panda_face"],
|
| 126 |
+
emojis["rabbit"],
|
| 127 |
+
emojis["polar_bear"],
|
| 128 |
+
emojis["tangerine"],
|
| 129 |
+
emojis["watermelon"],
|
| 130 |
+
emojis["pineapple"],
|
| 131 |
+
emojis["beer"],
|
| 132 |
+
emojis["curry"],
|
| 133 |
+
emojis["cake"],
|
| 134 |
+
emojis["snow_capped_mountain"],
|
| 135 |
+
emojis["volcano"],
|
| 136 |
+
emojis["bridge_at_night"],
|
| 137 |
+
emojis["kiwifruit"],
|
| 138 |
+
emojis["stadium"],
|
| 139 |
+
emojis["foggy"],
|
| 140 |
+
emojis["night_with_stars"],
|
| 141 |
+
emojis["cityscape"],
|
| 142 |
+
emojis["sunrise_over_mountains"],
|
| 143 |
+
emojis["sunrise"],
|
| 144 |
+
emojis["city_sunset"],
|
| 145 |
+
emojis["city_sunrise"],
|
| 146 |
+
],
|
| 147 |
+
},
|
| 148 |
+
{
|
| 149 |
+
id: "new-people",
|
| 150 |
+
name: "People",
|
| 151 |
+
emojis: [
|
| 152 |
+
emojis["child"],
|
| 153 |
+
emojis["boy"],
|
| 154 |
+
emojis["girl"],
|
| 155 |
+
emojis["adult"],
|
| 156 |
+
emojis["person_with_blond_hair"],
|
| 157 |
+
emojis["man"],
|
| 158 |
+
emojis["bearded_person"],
|
| 159 |
+
emojis["man_with_beard"],
|
| 160 |
+
emojis["woman_with_beard"],
|
| 161 |
+
emojis["red_haired_man"],
|
| 162 |
+
emojis["curly_haired_man"],
|
| 163 |
+
emojis["white_haired_man"],
|
| 164 |
+
emojis["bald_man"],
|
| 165 |
+
emojis["woman"],
|
| 166 |
+
emojis["red_haired_woman"],
|
| 167 |
+
emojis["red_haired_person"],
|
| 168 |
+
emojis["curly_haired_woman"],
|
| 169 |
+
emojis["curly_haired_person"],
|
| 170 |
+
emojis["white_haired_woman"],
|
| 171 |
+
emojis["white_haired_person"],
|
| 172 |
+
emojis["bald_woman"],
|
| 173 |
+
emojis["bald_person"],
|
| 174 |
+
emojis["blond-haired-woman"],
|
| 175 |
+
emojis["blond-haired-man"],
|
| 176 |
+
emojis["older_adult"],
|
| 177 |
+
emojis["older_man"],
|
| 178 |
+
emojis["older_woman"],
|
| 179 |
+
emojis["person_frowning"],
|
| 180 |
+
emojis["man-frowning"],
|
| 181 |
+
emojis["woman-frowning"],
|
| 182 |
+
emojis["person_with_pouting_face"],
|
| 183 |
+
emojis["man-pouting"],
|
| 184 |
+
emojis["woman-pouting"],
|
| 185 |
+
emojis["health_worker"],
|
| 186 |
+
emojis["male-doctor"],
|
| 187 |
+
emojis["female-doctor"],
|
| 188 |
+
emojis["student"],
|
| 189 |
+
emojis["male-student"],
|
| 190 |
+
emojis["female-student"],
|
| 191 |
+
emojis["teacher"],
|
| 192 |
+
emojis["male-teacher"],
|
| 193 |
+
emojis["female-teacher"],
|
| 194 |
+
emojis["judge"],
|
| 195 |
+
emojis["male-judge"],
|
| 196 |
+
emojis["female-judge"],
|
| 197 |
+
emojis["farmer"],
|
| 198 |
+
emojis["male-farmer"],
|
| 199 |
+
emojis["female-farmer"],
|
| 200 |
+
emojis["cook"],
|
| 201 |
+
emojis["male-cook"],
|
| 202 |
+
emojis["female-cook"],
|
| 203 |
+
emojis["mechanic"],
|
| 204 |
+
emojis["male-mechanic"],
|
| 205 |
+
emojis["female-mechanic"],
|
| 206 |
+
emojis["office_worker"],
|
| 207 |
+
emojis["male-office-worker"],
|
| 208 |
+
emojis["female-office-worker"],
|
| 209 |
+
emojis["scientist"],
|
| 210 |
+
emojis["male-scientist"],
|
| 211 |
+
emojis["female-scientist"],
|
| 212 |
+
emojis["technologist"],
|
| 213 |
+
emojis["male-technologist"],
|
| 214 |
+
emojis["female-technologist"],
|
| 215 |
+
emojis["artist"],
|
| 216 |
+
emojis["male-artist"],
|
| 217 |
+
emojis["female-artist"],
|
| 218 |
+
emojis["astronaut"],
|
| 219 |
+
emojis["male-astronaut"],
|
| 220 |
+
emojis["female-astronaut"],
|
| 221 |
+
emojis["sleuth_or_spy"],
|
| 222 |
+
emojis["male-detective"],
|
| 223 |
+
emojis["female-detective"],
|
| 224 |
+
emojis["construction_worker"],
|
| 225 |
+
emojis["male-construction-worker"],
|
| 226 |
+
emojis["female-construction-worker"],
|
| 227 |
+
emojis["person_with_crown"],
|
| 228 |
+
emojis["prince"],
|
| 229 |
+
emojis["princess"],
|
| 230 |
+
emojis["person_in_tuxedo"],
|
| 231 |
+
emojis["man_in_tuxedo"],
|
| 232 |
+
emojis["woman_in_tuxedo"],
|
| 233 |
+
emojis["bride_with_veil"],
|
| 234 |
+
emojis["man_with_veil"],
|
| 235 |
+
emojis["woman_with_veil"],
|
| 236 |
+
],
|
| 237 |
+
},
|
| 238 |
+
];
|
| 239 |
+
export const EmojiSelector: FC<{ onSelect: (e: EmojiData) => void }> = ({
|
| 240 |
+
onSelect,
|
| 241 |
+
}) => {
|
| 242 |
+
const isSmallScreen = useMediaQuery({ query: "(max-width: 768px)" });
|
| 243 |
+
|
| 244 |
+
return (
|
| 245 |
+
<Picker
|
| 246 |
+
exceptEmojis={exceptEmojis}
|
| 247 |
+
dynamicWidth={true}
|
| 248 |
+
custom={custom}
|
| 249 |
+
categories={[
|
| 250 |
+
"recommend",
|
| 251 |
+
"new-people",
|
| 252 |
+
"nature",
|
| 253 |
+
"foods",
|
| 254 |
+
"activity",
|
| 255 |
+
"places",
|
| 256 |
+
"objects",
|
| 257 |
+
]}
|
| 258 |
+
theme="dark"
|
| 259 |
+
categoryIcons={categoryIcons}
|
| 260 |
+
searchPosition={isSmallScreen ? "none" : "bottom"}
|
| 261 |
+
navPosition={isSmallScreen ? "none" : "top"}
|
| 262 |
+
previewPosition={isSmallScreen ? "none" : "bottom"}
|
| 263 |
+
data={data}
|
| 264 |
+
onEmojiSelect={onSelect}
|
| 265 |
+
/>
|
| 266 |
+
);
|
| 267 |
+
};
|
src/components/github.tsx
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from "react";
|
| 2 |
+
|
| 3 |
+
export function GithubForkRibbon() {
|
| 4 |
+
return (
|
| 5 |
+
<div className="hidden md:block overflow-hidden w-[150px] h-[150px] absolute top-0 z-50 right-0">
|
| 6 |
+
<div className="bg-amber-600 text-zinc-100 shadow-2xl absolute p-1 z-50 top-[35px] right-[-45px] rotate-45">
|
| 7 |
+
<a
|
| 8 |
+
className="w-[200px] inline-block p-1 text-center"
|
| 9 |
+
href="https://github.com/leptonai/tryemoji"
|
| 10 |
+
target="_blank"
|
| 11 |
+
>
|
| 12 |
+
Fork me on GitHub
|
| 13 |
+
</a>
|
| 14 |
+
</div>
|
| 15 |
+
</div>
|
| 16 |
+
);
|
| 17 |
+
}
|
src/components/try-emoji.tsx
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
import { Dice } from "@/components/dice";
|
| 3 |
+
import { EmojiSelector } from "@/components/emoji-selector";
|
| 4 |
+
import { GithubForkRibbon } from "@/components/github";
|
| 5 |
+
import {
|
| 6 |
+
Select,
|
| 7 |
+
SelectContent,
|
| 8 |
+
SelectItem,
|
| 9 |
+
SelectTrigger,
|
| 10 |
+
SelectValue,
|
| 11 |
+
} from "@/components/ui/select";
|
| 12 |
+
import { Slider } from "@/components/ui/slider";
|
| 13 |
+
import { Toaster } from "@/components/ui/toaster";
|
| 14 |
+
import {
|
| 15 |
+
Tooltip,
|
| 16 |
+
TooltipContent,
|
| 17 |
+
TooltipProvider,
|
| 18 |
+
TooltipTrigger,
|
| 19 |
+
} from "@/components/ui/tooltip";
|
| 20 |
+
import { useToast } from "@/components/ui/use-toast";
|
| 21 |
+
import { presetImage, presetArtStyles } from "@/util/presets";
|
| 22 |
+
import { usePrevious } from "@/util/use-previous";
|
| 23 |
+
import { useResponse } from "@/util/use-response";
|
| 24 |
+
import { getShareUrl, Option, useShare } from "@/util/use-share";
|
| 25 |
+
import { clsx } from "clsx";
|
| 26 |
+
import { Check, Download, Share2 } from "lucide-react";
|
| 27 |
+
import { useEffect, useMemo, useState } from "react";
|
| 28 |
+
import {
|
| 29 |
+
FacebookIcon,
|
| 30 |
+
FacebookShareButton,
|
| 31 |
+
LinkedinIcon,
|
| 32 |
+
LinkedinShareButton,
|
| 33 |
+
TwitterShareButton,
|
| 34 |
+
XIcon,
|
| 35 |
+
} from "react-share";
|
| 36 |
+
import { setEmojiFavicon } from "@/util/set-emoji-favicon";
|
| 37 |
+
|
| 38 |
+
export default function TryEmoji() {
|
| 39 |
+
const { option: presetOption, hasShare } = useShare();
|
| 40 |
+
const { toast } = useToast();
|
| 41 |
+
const [emoji, setEmoji] = useState({
|
| 42 |
+
emoji: presetOption.emoji,
|
| 43 |
+
name: presetOption.name,
|
| 44 |
+
});
|
| 45 |
+
const [preset, setPreset] = useState(
|
| 46 |
+
presetArtStyles.find((p) => p.prompt === presetOption.prompt)!,
|
| 47 |
+
);
|
| 48 |
+
const [strength, setStrength] = useState(presetOption.strength);
|
| 49 |
+
const [seed, setSeed] = useState(presetOption.seed);
|
| 50 |
+
|
| 51 |
+
const shareOption: Option = useMemo(() => {
|
| 52 |
+
return {
|
| 53 |
+
emoji: emoji.emoji,
|
| 54 |
+
name: emoji.name,
|
| 55 |
+
prompt: preset.prompt,
|
| 56 |
+
seed: seed,
|
| 57 |
+
strength: strength,
|
| 58 |
+
};
|
| 59 |
+
}, [emoji.emoji, emoji.name, preset.prompt, seed, strength]);
|
| 60 |
+
|
| 61 |
+
const { image, loading } = useResponse(
|
| 62 |
+
hasShare,
|
| 63 |
+
emoji.emoji,
|
| 64 |
+
emoji.name,
|
| 65 |
+
preset.prompt,
|
| 66 |
+
strength,
|
| 67 |
+
seed,
|
| 68 |
+
);
|
| 69 |
+
const previousImage = usePrevious(image);
|
| 70 |
+
|
| 71 |
+
const mergedImage = useMemo(
|
| 72 |
+
() => image || previousImage || presetImage,
|
| 73 |
+
[image, previousImage],
|
| 74 |
+
);
|
| 75 |
+
|
| 76 |
+
useEffect(() => {
|
| 77 |
+
setEmojiFavicon(emoji.emoji);
|
| 78 |
+
}, [emoji.emoji]);
|
| 79 |
+
|
| 80 |
+
const shareKey = useMemo(() => {
|
| 81 |
+
return getShareUrl(shareOption);
|
| 82 |
+
}, [shareOption]);
|
| 83 |
+
|
| 84 |
+
const shareUrl = useMemo(() => {
|
| 85 |
+
return `https://tryemoji.com?share=${shareKey}`;
|
| 86 |
+
}, [shareKey]);
|
| 87 |
+
|
| 88 |
+
const warmOrg: Promise<void> = useMemo(() => {
|
| 89 |
+
if (image) {
|
| 90 |
+
return fetch("/api/share", {
|
| 91 |
+
method: "POST",
|
| 92 |
+
body: JSON.stringify({
|
| 93 |
+
image: image,
|
| 94 |
+
key: shareKey,
|
| 95 |
+
}),
|
| 96 |
+
}).then();
|
| 97 |
+
} else {
|
| 98 |
+
return new Promise((resolve) => resolve());
|
| 99 |
+
}
|
| 100 |
+
}, [image, shareKey]);
|
| 101 |
+
|
| 102 |
+
return (
|
| 103 |
+
<TooltipProvider delayDuration={50}>
|
| 104 |
+
<Toaster />
|
| 105 |
+
<div className="min-h-screen flex flex-col gap-4 bg-zinc-950 items-center justify-center py-4 md:py-12">
|
| 106 |
+
<GithubForkRibbon></GithubForkRibbon>
|
| 107 |
+
<div className="text-6xl text-zinc-100">
|
| 108 |
+
{emoji.emoji || "🐤"} tryEmoji{" "}
|
| 109 |
+
</div>
|
| 110 |
+
<div className="text-xl text-zinc-100">
|
| 111 |
+
Turn emoji into amazing artwork via AI
|
| 112 |
+
</div>
|
| 113 |
+
<div className="flex items-center justify-center flex-col md:flex-row gap-2 md:gap-4">
|
| 114 |
+
<div className="flex-0 w-full md:w-80">
|
| 115 |
+
<EmojiSelector
|
| 116 |
+
onSelect={(e) => {
|
| 117 |
+
const prefix =
|
| 118 |
+
e.keywords.indexOf("animal") > -1 ? "super cute" : "";
|
| 119 |
+
const keyword = e.keywords.join(", ");
|
| 120 |
+
const emoji = e.native;
|
| 121 |
+
const name = `${prefix} ${e.name}, ${keyword}`;
|
| 122 |
+
setEmoji({ emoji, name });
|
| 123 |
+
}}
|
| 124 |
+
></EmojiSelector>
|
| 125 |
+
</div>
|
| 126 |
+
<div className="flex-1">
|
| 127 |
+
<div className="max-w-[100vw] h-auto md:h-[512px] w-[512px] rounded-lg overflow-hidden relative">
|
| 128 |
+
<img src={mergedImage} className="h-full w-full object-contain" />
|
| 129 |
+
<div
|
| 130 |
+
className={clsx("transition absolute inset-0", {
|
| 131 |
+
"backdrop-blur-xl": loading,
|
| 132 |
+
})}
|
| 133 |
+
></div>
|
| 134 |
+
<div className="hidden absolute top-2 right-2 md:flex gap-2 items-center">
|
| 135 |
+
<FacebookShareButton
|
| 136 |
+
beforeOnClick={() => warmOrg}
|
| 137 |
+
url={shareUrl}
|
| 138 |
+
>
|
| 139 |
+
<FacebookIcon className="rounded" size={24}></FacebookIcon>
|
| 140 |
+
</FacebookShareButton>
|
| 141 |
+
<TwitterShareButton onClick={() => warmOrg} url={shareUrl}>
|
| 142 |
+
<XIcon className="rounded" size={24} />
|
| 143 |
+
</TwitterShareButton>
|
| 144 |
+
<LinkedinShareButton onClick={() => warmOrg} url={shareUrl}>
|
| 145 |
+
<LinkedinIcon className="rounded" size={24} />
|
| 146 |
+
</LinkedinShareButton>
|
| 147 |
+
<Tooltip>
|
| 148 |
+
<TooltipTrigger asChild>
|
| 149 |
+
<button
|
| 150 |
+
onClick={() => {
|
| 151 |
+
warmOrg.then(() => {
|
| 152 |
+
navigator.clipboard.writeText(shareUrl).then(() => {
|
| 153 |
+
toast({
|
| 154 |
+
description: (
|
| 155 |
+
<div className="flex gap-2 text-sm items-center">
|
| 156 |
+
<Check className="text-green-500"></Check>
|
| 157 |
+
Copied, paste to share
|
| 158 |
+
</div>
|
| 159 |
+
),
|
| 160 |
+
});
|
| 161 |
+
});
|
| 162 |
+
});
|
| 163 |
+
}}
|
| 164 |
+
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"
|
| 165 |
+
>
|
| 166 |
+
<Share2 size={16}></Share2>
|
| 167 |
+
</button>
|
| 168 |
+
</TooltipTrigger>
|
| 169 |
+
<TooltipContent>
|
| 170 |
+
<p>Share</p>
|
| 171 |
+
</TooltipContent>
|
| 172 |
+
</Tooltip>
|
| 173 |
+
</div>
|
| 174 |
+
<div className="absolute bottom-2 left-2 right-2 flex gap-2 flex-wrap">
|
| 175 |
+
<div className="flex flex-auto gap-2 w-full md:w-auto">
|
| 176 |
+
<div className="text-xl text-zinc-100">AI</div>
|
| 177 |
+
<Tooltip>
|
| 178 |
+
<TooltipTrigger asChild>
|
| 179 |
+
<Slider
|
| 180 |
+
className="flex-1"
|
| 181 |
+
defaultValue={[strength]}
|
| 182 |
+
onValueChange={(v) => setStrength(v[0])}
|
| 183 |
+
max={0.7}
|
| 184 |
+
min={0.5}
|
| 185 |
+
step={0.025}
|
| 186 |
+
/>
|
| 187 |
+
</TooltipTrigger>
|
| 188 |
+
<TooltipContent>
|
| 189 |
+
<p>AI strength</p>
|
| 190 |
+
</TooltipContent>
|
| 191 |
+
</Tooltip>
|
| 192 |
+
</div>
|
| 193 |
+
<div className="flex flex-auto md:flex-grow-0 gap-2 w-full md:w-auto">
|
| 194 |
+
<Select
|
| 195 |
+
value={preset.artist}
|
| 196 |
+
onValueChange={(value) =>
|
| 197 |
+
setPreset(
|
| 198 |
+
presetArtStyles.find((p) => p.artist === value)!,
|
| 199 |
+
)
|
| 200 |
+
}
|
| 201 |
+
>
|
| 202 |
+
<Tooltip>
|
| 203 |
+
<TooltipTrigger asChild>
|
| 204 |
+
<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">
|
| 205 |
+
<SelectValue placeholder="Select a fruit" />
|
| 206 |
+
</SelectTrigger>
|
| 207 |
+
</TooltipTrigger>
|
| 208 |
+
<TooltipContent>
|
| 209 |
+
<p>Art style</p>
|
| 210 |
+
</TooltipContent>
|
| 211 |
+
</Tooltip>
|
| 212 |
+
|
| 213 |
+
<SelectContent>
|
| 214 |
+
{presetArtStyles.map((p) => (
|
| 215 |
+
<SelectItem key={p.artist} value={p.artist}>
|
| 216 |
+
{p.artist}
|
| 217 |
+
</SelectItem>
|
| 218 |
+
))}
|
| 219 |
+
</SelectContent>
|
| 220 |
+
</Select>
|
| 221 |
+
<Tooltip>
|
| 222 |
+
<TooltipTrigger asChild>
|
| 223 |
+
<button
|
| 224 |
+
onClick={() => {
|
| 225 |
+
setSeed(Math.floor(Math.random() * 2159232));
|
| 226 |
+
}}
|
| 227 |
+
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"
|
| 228 |
+
>
|
| 229 |
+
<Dice></Dice>
|
| 230 |
+
</button>
|
| 231 |
+
</TooltipTrigger>
|
| 232 |
+
<TooltipContent>
|
| 233 |
+
<p>Random</p>
|
| 234 |
+
</TooltipContent>
|
| 235 |
+
</Tooltip>
|
| 236 |
+
<Tooltip>
|
| 237 |
+
<TooltipTrigger asChild>
|
| 238 |
+
<a
|
| 239 |
+
href={image}
|
| 240 |
+
download
|
| 241 |
+
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"
|
| 242 |
+
>
|
| 243 |
+
<Download />
|
| 244 |
+
</a>
|
| 245 |
+
</TooltipTrigger>
|
| 246 |
+
<TooltipContent>
|
| 247 |
+
<p>Download</p>
|
| 248 |
+
</TooltipContent>
|
| 249 |
+
</Tooltip>
|
| 250 |
+
</div>
|
| 251 |
+
</div>
|
| 252 |
+
</div>
|
| 253 |
+
</div>
|
| 254 |
+
</div>
|
| 255 |
+
<div className="text-xs text-zinc-500 font-sans mt-8 flex gap-2">
|
| 256 |
+
<a
|
| 257 |
+
className="hover:text-zinc-100"
|
| 258 |
+
href="https://lepton.ai"
|
| 259 |
+
target="_blank"
|
| 260 |
+
>
|
| 261 |
+
Powered by Lepton AI
|
| 262 |
+
</a>
|
| 263 |
+
|
|
| 264 |
+
<a
|
| 265 |
+
className="hover:text-zinc-100"
|
| 266 |
+
href="https://github.com/leptonai/tryemoji"
|
| 267 |
+
target="_blank"
|
| 268 |
+
>
|
| 269 |
+
Github
|
| 270 |
+
</a>
|
| 271 |
+
</div>
|
| 272 |
+
</div>
|
| 273 |
+
</TooltipProvider>
|
| 274 |
+
);
|
| 275 |
+
}
|
src/components/ui/select.tsx
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import * as React from "react";
|
| 4 |
+
import * as SelectPrimitive from "@radix-ui/react-select";
|
| 5 |
+
import { Check, ChevronDown, ChevronUp } from "lucide-react";
|
| 6 |
+
|
| 7 |
+
import { cn } from "@/components/ui/utils";
|
| 8 |
+
|
| 9 |
+
const Select = SelectPrimitive.Root;
|
| 10 |
+
|
| 11 |
+
const SelectGroup = SelectPrimitive.Group;
|
| 12 |
+
|
| 13 |
+
const SelectValue = SelectPrimitive.Value;
|
| 14 |
+
|
| 15 |
+
const SelectTrigger = React.forwardRef<
|
| 16 |
+
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
| 17 |
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
| 18 |
+
>(({ className, children, ...props }, ref) => (
|
| 19 |
+
<SelectPrimitive.Trigger
|
| 20 |
+
ref={ref}
|
| 21 |
+
className={cn(
|
| 22 |
+
"flex w-full items-center justify-between rounded-md border border-input bg-background px-3 py-1 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
| 23 |
+
className,
|
| 24 |
+
)}
|
| 25 |
+
{...props}
|
| 26 |
+
>
|
| 27 |
+
{children}
|
| 28 |
+
<SelectPrimitive.Icon asChild>
|
| 29 |
+
<ChevronDown className="h-4 w-4 opacity-50" />
|
| 30 |
+
</SelectPrimitive.Icon>
|
| 31 |
+
</SelectPrimitive.Trigger>
|
| 32 |
+
));
|
| 33 |
+
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
|
| 34 |
+
|
| 35 |
+
const SelectScrollUpButton = React.forwardRef<
|
| 36 |
+
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
| 37 |
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
| 38 |
+
>(({ className, ...props }, ref) => (
|
| 39 |
+
<SelectPrimitive.ScrollUpButton
|
| 40 |
+
ref={ref}
|
| 41 |
+
className={cn(
|
| 42 |
+
"flex cursor-default items-center justify-center py-1",
|
| 43 |
+
className,
|
| 44 |
+
)}
|
| 45 |
+
{...props}
|
| 46 |
+
>
|
| 47 |
+
<ChevronUp className="h-4 w-4" />
|
| 48 |
+
</SelectPrimitive.ScrollUpButton>
|
| 49 |
+
));
|
| 50 |
+
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
|
| 51 |
+
|
| 52 |
+
const SelectScrollDownButton = React.forwardRef<
|
| 53 |
+
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
| 54 |
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
| 55 |
+
>(({ className, ...props }, ref) => (
|
| 56 |
+
<SelectPrimitive.ScrollDownButton
|
| 57 |
+
ref={ref}
|
| 58 |
+
className={cn(
|
| 59 |
+
"flex cursor-default items-center justify-center py-1",
|
| 60 |
+
className,
|
| 61 |
+
)}
|
| 62 |
+
{...props}
|
| 63 |
+
>
|
| 64 |
+
<ChevronDown className="h-4 w-4" />
|
| 65 |
+
</SelectPrimitive.ScrollDownButton>
|
| 66 |
+
));
|
| 67 |
+
SelectScrollDownButton.displayName =
|
| 68 |
+
SelectPrimitive.ScrollDownButton.displayName;
|
| 69 |
+
|
| 70 |
+
const SelectContent = React.forwardRef<
|
| 71 |
+
React.ElementRef<typeof SelectPrimitive.Content>,
|
| 72 |
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
| 73 |
+
>(({ className, children, position = "popper", ...props }, ref) => (
|
| 74 |
+
<SelectPrimitive.Portal>
|
| 75 |
+
<SelectPrimitive.Content
|
| 76 |
+
ref={ref}
|
| 77 |
+
className={cn(
|
| 78 |
+
"bg-zinc-900 relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
| 79 |
+
position === "popper" &&
|
| 80 |
+
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
| 81 |
+
className,
|
| 82 |
+
)}
|
| 83 |
+
position={position}
|
| 84 |
+
{...props}
|
| 85 |
+
>
|
| 86 |
+
<SelectScrollUpButton />
|
| 87 |
+
<SelectPrimitive.Viewport
|
| 88 |
+
className={cn(
|
| 89 |
+
"p-0 bg-zinc-900",
|
| 90 |
+
position === "popper" &&
|
| 91 |
+
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
|
| 92 |
+
)}
|
| 93 |
+
>
|
| 94 |
+
{children}
|
| 95 |
+
</SelectPrimitive.Viewport>
|
| 96 |
+
<SelectScrollDownButton />
|
| 97 |
+
</SelectPrimitive.Content>
|
| 98 |
+
</SelectPrimitive.Portal>
|
| 99 |
+
));
|
| 100 |
+
SelectContent.displayName = SelectPrimitive.Content.displayName;
|
| 101 |
+
|
| 102 |
+
const SelectLabel = React.forwardRef<
|
| 103 |
+
React.ElementRef<typeof SelectPrimitive.Label>,
|
| 104 |
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
| 105 |
+
>(({ className, ...props }, ref) => (
|
| 106 |
+
<SelectPrimitive.Label
|
| 107 |
+
ref={ref}
|
| 108 |
+
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
|
| 109 |
+
{...props}
|
| 110 |
+
/>
|
| 111 |
+
));
|
| 112 |
+
SelectLabel.displayName = SelectPrimitive.Label.displayName;
|
| 113 |
+
|
| 114 |
+
const SelectItem = React.forwardRef<
|
| 115 |
+
React.ElementRef<typeof SelectPrimitive.Item>,
|
| 116 |
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
| 117 |
+
>(({ className, children, ...props }, ref) => (
|
| 118 |
+
<SelectPrimitive.Item
|
| 119 |
+
ref={ref}
|
| 120 |
+
className={cn(
|
| 121 |
+
"relative bg-zinc-900 text-zinc-100 flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
| 122 |
+
className,
|
| 123 |
+
)}
|
| 124 |
+
{...props}
|
| 125 |
+
>
|
| 126 |
+
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
| 127 |
+
<SelectPrimitive.ItemIndicator>
|
| 128 |
+
<Check className="h-4 w-4" />
|
| 129 |
+
</SelectPrimitive.ItemIndicator>
|
| 130 |
+
</span>
|
| 131 |
+
|
| 132 |
+
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
| 133 |
+
</SelectPrimitive.Item>
|
| 134 |
+
));
|
| 135 |
+
SelectItem.displayName = SelectPrimitive.Item.displayName;
|
| 136 |
+
|
| 137 |
+
const SelectSeparator = React.forwardRef<
|
| 138 |
+
React.ElementRef<typeof SelectPrimitive.Separator>,
|
| 139 |
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
| 140 |
+
>(({ className, ...props }, ref) => (
|
| 141 |
+
<SelectPrimitive.Separator
|
| 142 |
+
ref={ref}
|
| 143 |
+
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
| 144 |
+
{...props}
|
| 145 |
+
/>
|
| 146 |
+
));
|
| 147 |
+
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
|
| 148 |
+
|
| 149 |
+
export {
|
| 150 |
+
Select,
|
| 151 |
+
SelectGroup,
|
| 152 |
+
SelectValue,
|
| 153 |
+
SelectTrigger,
|
| 154 |
+
SelectContent,
|
| 155 |
+
SelectLabel,
|
| 156 |
+
SelectItem,
|
| 157 |
+
SelectSeparator,
|
| 158 |
+
SelectScrollUpButton,
|
| 159 |
+
SelectScrollDownButton,
|
| 160 |
+
};
|
src/components/ui/slider.tsx
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import * as React from "react";
|
| 4 |
+
import * as SliderPrimitive from "@radix-ui/react-slider";
|
| 5 |
+
|
| 6 |
+
import { cn } from "@/components/ui/utils";
|
| 7 |
+
|
| 8 |
+
const Slider = React.forwardRef<
|
| 9 |
+
React.ElementRef<typeof SliderPrimitive.Root>,
|
| 10 |
+
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
|
| 11 |
+
>(({ className, ...props }, ref) => (
|
| 12 |
+
<SliderPrimitive.Root
|
| 13 |
+
ref={ref}
|
| 14 |
+
className={cn(
|
| 15 |
+
"relative flex w-full touch-none select-none items-center",
|
| 16 |
+
className,
|
| 17 |
+
)}
|
| 18 |
+
{...props}
|
| 19 |
+
>
|
| 20 |
+
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-amber-500">
|
| 21 |
+
<SliderPrimitive.Range className="absolute h-full bg-amber-600" />
|
| 22 |
+
</SliderPrimitive.Track>
|
| 23 |
+
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-amber-500 bg-amber-600 ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
|
| 24 |
+
</SliderPrimitive.Root>
|
| 25 |
+
));
|
| 26 |
+
Slider.displayName = SliderPrimitive.Root.displayName;
|
| 27 |
+
|
| 28 |
+
export { Slider };
|
src/components/ui/toast.tsx
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react";
|
| 2 |
+
import * as ToastPrimitives from "@radix-ui/react-toast";
|
| 3 |
+
import { cva, type VariantProps } from "class-variance-authority";
|
| 4 |
+
import { X } from "lucide-react";
|
| 5 |
+
|
| 6 |
+
import { cn } from "@/components/ui/utils";
|
| 7 |
+
|
| 8 |
+
const ToastProvider = ToastPrimitives.Provider;
|
| 9 |
+
|
| 10 |
+
const ToastViewport = React.forwardRef<
|
| 11 |
+
React.ElementRef<typeof ToastPrimitives.Viewport>,
|
| 12 |
+
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
|
| 13 |
+
>(({ className, ...props }, ref) => (
|
| 14 |
+
<ToastPrimitives.Viewport
|
| 15 |
+
ref={ref}
|
| 16 |
+
className={cn(
|
| 17 |
+
"fixed bottom-0 right-0 z-[100] flex max-h-screen w-fit flex-col-reverse p-4 md:max-w-[420px]",
|
| 18 |
+
className,
|
| 19 |
+
)}
|
| 20 |
+
{...props}
|
| 21 |
+
/>
|
| 22 |
+
));
|
| 23 |
+
ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
|
| 24 |
+
|
| 25 |
+
const toastVariants = cva(
|
| 26 |
+
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md p-3 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
|
| 27 |
+
{
|
| 28 |
+
variants: {
|
| 29 |
+
variant: {
|
| 30 |
+
default: "bg-amber-600 text-zinc-100",
|
| 31 |
+
destructive:
|
| 32 |
+
"destructive group border-destructive bg-destructive text-destructive-zinc-100",
|
| 33 |
+
},
|
| 34 |
+
},
|
| 35 |
+
defaultVariants: {
|
| 36 |
+
variant: "default",
|
| 37 |
+
},
|
| 38 |
+
},
|
| 39 |
+
);
|
| 40 |
+
|
| 41 |
+
const Toast = React.forwardRef<
|
| 42 |
+
React.ElementRef<typeof ToastPrimitives.Root>,
|
| 43 |
+
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
|
| 44 |
+
VariantProps<typeof toastVariants>
|
| 45 |
+
>(({ className, variant, ...props }, ref) => {
|
| 46 |
+
return (
|
| 47 |
+
<ToastPrimitives.Root
|
| 48 |
+
ref={ref}
|
| 49 |
+
className={cn(toastVariants({ variant }), className)}
|
| 50 |
+
{...props}
|
| 51 |
+
/>
|
| 52 |
+
);
|
| 53 |
+
});
|
| 54 |
+
Toast.displayName = ToastPrimitives.Root.displayName;
|
| 55 |
+
|
| 56 |
+
const ToastAction = React.forwardRef<
|
| 57 |
+
React.ElementRef<typeof ToastPrimitives.Action>,
|
| 58 |
+
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
|
| 59 |
+
>(({ className, ...props }, ref) => (
|
| 60 |
+
<ToastPrimitives.Action
|
| 61 |
+
ref={ref}
|
| 62 |
+
className={cn(
|
| 63 |
+
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-amber-600 transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-zinc-100 group-[.destructive]:focus:ring-destructive",
|
| 64 |
+
className,
|
| 65 |
+
)}
|
| 66 |
+
{...props}
|
| 67 |
+
/>
|
| 68 |
+
));
|
| 69 |
+
ToastAction.displayName = ToastPrimitives.Action.displayName;
|
| 70 |
+
|
| 71 |
+
const ToastClose = React.forwardRef<
|
| 72 |
+
React.ElementRef<typeof ToastPrimitives.Close>,
|
| 73 |
+
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
|
| 74 |
+
>(({ className, ...props }, ref) => (
|
| 75 |
+
<ToastPrimitives.Close
|
| 76 |
+
ref={ref}
|
| 77 |
+
className={cn(
|
| 78 |
+
"absolute right-2 top-2 rounded-md p-1 text-zinc-100/50 opacity-0 transition-opacity hover:text-zinc-100 focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
|
| 79 |
+
className,
|
| 80 |
+
)}
|
| 81 |
+
toast-close=""
|
| 82 |
+
{...props}
|
| 83 |
+
>
|
| 84 |
+
<X className="h-4 w-4" />
|
| 85 |
+
</ToastPrimitives.Close>
|
| 86 |
+
));
|
| 87 |
+
ToastClose.displayName = ToastPrimitives.Close.displayName;
|
| 88 |
+
|
| 89 |
+
const ToastTitle = React.forwardRef<
|
| 90 |
+
React.ElementRef<typeof ToastPrimitives.Title>,
|
| 91 |
+
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
|
| 92 |
+
>(({ className, ...props }, ref) => (
|
| 93 |
+
<ToastPrimitives.Title
|
| 94 |
+
ref={ref}
|
| 95 |
+
className={cn("text-sm font-semibold", className)}
|
| 96 |
+
{...props}
|
| 97 |
+
/>
|
| 98 |
+
));
|
| 99 |
+
ToastTitle.displayName = ToastPrimitives.Title.displayName;
|
| 100 |
+
|
| 101 |
+
const ToastDescription = React.forwardRef<
|
| 102 |
+
React.ElementRef<typeof ToastPrimitives.Description>,
|
| 103 |
+
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
|
| 104 |
+
>(({ className, ...props }, ref) => (
|
| 105 |
+
<ToastPrimitives.Description
|
| 106 |
+
ref={ref}
|
| 107 |
+
className={cn("text-sm opacity-90", className)}
|
| 108 |
+
{...props}
|
| 109 |
+
/>
|
| 110 |
+
));
|
| 111 |
+
ToastDescription.displayName = ToastPrimitives.Description.displayName;
|
| 112 |
+
|
| 113 |
+
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;
|
| 114 |
+
|
| 115 |
+
type ToastActionElement = React.ReactElement<typeof ToastAction>;
|
| 116 |
+
|
| 117 |
+
export {
|
| 118 |
+
type ToastProps,
|
| 119 |
+
type ToastActionElement,
|
| 120 |
+
ToastProvider,
|
| 121 |
+
ToastViewport,
|
| 122 |
+
Toast,
|
| 123 |
+
ToastTitle,
|
| 124 |
+
ToastDescription,
|
| 125 |
+
ToastClose,
|
| 126 |
+
ToastAction,
|
| 127 |
+
};
|
src/components/ui/toaster.tsx
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import {
|
| 4 |
+
Toast,
|
| 5 |
+
ToastClose,
|
| 6 |
+
ToastDescription,
|
| 7 |
+
ToastProvider,
|
| 8 |
+
ToastTitle,
|
| 9 |
+
ToastViewport,
|
| 10 |
+
} from "@/components/ui/toast";
|
| 11 |
+
import { useToast } from "@/components/ui/use-toast";
|
| 12 |
+
|
| 13 |
+
export function Toaster() {
|
| 14 |
+
const { toasts } = useToast();
|
| 15 |
+
|
| 16 |
+
return (
|
| 17 |
+
<ToastProvider>
|
| 18 |
+
{toasts.map(function ({ id, title, description, action, ...props }) {
|
| 19 |
+
return (
|
| 20 |
+
<Toast key={id} {...props}>
|
| 21 |
+
<div className="grid gap-1">
|
| 22 |
+
{title && <ToastTitle>{title}</ToastTitle>}
|
| 23 |
+
{description && (
|
| 24 |
+
<ToastDescription>{description}</ToastDescription>
|
| 25 |
+
)}
|
| 26 |
+
</div>
|
| 27 |
+
{action}
|
| 28 |
+
<ToastClose />
|
| 29 |
+
</Toast>
|
| 30 |
+
);
|
| 31 |
+
})}
|
| 32 |
+
<ToastViewport />
|
| 33 |
+
</ToastProvider>
|
| 34 |
+
);
|
| 35 |
+
}
|
src/components/ui/tooltip.tsx
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import * as React from "react";
|
| 4 |
+
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
| 5 |
+
|
| 6 |
+
import { cn } from "@/components/ui/utils";
|
| 7 |
+
|
| 8 |
+
const TooltipProvider = TooltipPrimitive.Provider;
|
| 9 |
+
|
| 10 |
+
const Tooltip = TooltipPrimitive.Root;
|
| 11 |
+
|
| 12 |
+
const TooltipTrigger = TooltipPrimitive.Trigger;
|
| 13 |
+
|
| 14 |
+
const TooltipContent = React.forwardRef<
|
| 15 |
+
React.ElementRef<typeof TooltipPrimitive.Content>,
|
| 16 |
+
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
| 17 |
+
>(({ className, sideOffset = 4, ...props }, ref) => (
|
| 18 |
+
<TooltipPrimitive.Content
|
| 19 |
+
ref={ref}
|
| 20 |
+
sideOffset={sideOffset}
|
| 21 |
+
className={cn(
|
| 22 |
+
"z-50 overflow-hidden rounded-md px-3 py-1.5 text-sm text-zinc-100 bg-amber-800 shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
| 23 |
+
className,
|
| 24 |
+
)}
|
| 25 |
+
{...props}
|
| 26 |
+
/>
|
| 27 |
+
));
|
| 28 |
+
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
|
| 29 |
+
|
| 30 |
+
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
src/components/ui/use-toast.tsx
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Inspired by react-hot-toast library
|
| 2 |
+
import * as React from "react";
|
| 3 |
+
|
| 4 |
+
import type { ToastActionElement, ToastProps } from "@/components/ui/toast";
|
| 5 |
+
|
| 6 |
+
const TOAST_LIMIT = 1;
|
| 7 |
+
const TOAST_REMOVE_DELAY = 1000000;
|
| 8 |
+
|
| 9 |
+
type ToasterToast = ToastProps & {
|
| 10 |
+
id: string;
|
| 11 |
+
title?: React.ReactNode;
|
| 12 |
+
description?: React.ReactNode;
|
| 13 |
+
action?: ToastActionElement;
|
| 14 |
+
};
|
| 15 |
+
|
| 16 |
+
const actionTypes = {
|
| 17 |
+
ADD_TOAST: "ADD_TOAST",
|
| 18 |
+
UPDATE_TOAST: "UPDATE_TOAST",
|
| 19 |
+
DISMISS_TOAST: "DISMISS_TOAST",
|
| 20 |
+
REMOVE_TOAST: "REMOVE_TOAST",
|
| 21 |
+
} as const;
|
| 22 |
+
|
| 23 |
+
let count = 0;
|
| 24 |
+
|
| 25 |
+
function genId() {
|
| 26 |
+
count = (count + 1) % Number.MAX_SAFE_INTEGER;
|
| 27 |
+
return count.toString();
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
type ActionType = typeof actionTypes;
|
| 31 |
+
|
| 32 |
+
type Action =
|
| 33 |
+
| {
|
| 34 |
+
type: ActionType["ADD_TOAST"];
|
| 35 |
+
toast: ToasterToast;
|
| 36 |
+
}
|
| 37 |
+
| {
|
| 38 |
+
type: ActionType["UPDATE_TOAST"];
|
| 39 |
+
toast: Partial<ToasterToast>;
|
| 40 |
+
}
|
| 41 |
+
| {
|
| 42 |
+
type: ActionType["DISMISS_TOAST"];
|
| 43 |
+
toastId?: ToasterToast["id"];
|
| 44 |
+
}
|
| 45 |
+
| {
|
| 46 |
+
type: ActionType["REMOVE_TOAST"];
|
| 47 |
+
toastId?: ToasterToast["id"];
|
| 48 |
+
};
|
| 49 |
+
|
| 50 |
+
interface State {
|
| 51 |
+
toasts: ToasterToast[];
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
|
| 55 |
+
|
| 56 |
+
const addToRemoveQueue = (toastId: string) => {
|
| 57 |
+
if (toastTimeouts.has(toastId)) {
|
| 58 |
+
return;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
const timeout = setTimeout(() => {
|
| 62 |
+
toastTimeouts.delete(toastId);
|
| 63 |
+
dispatch({
|
| 64 |
+
type: "REMOVE_TOAST",
|
| 65 |
+
toastId: toastId,
|
| 66 |
+
});
|
| 67 |
+
}, TOAST_REMOVE_DELAY);
|
| 68 |
+
|
| 69 |
+
toastTimeouts.set(toastId, timeout);
|
| 70 |
+
};
|
| 71 |
+
|
| 72 |
+
export const reducer = (state: State, action: Action): State => {
|
| 73 |
+
switch (action.type) {
|
| 74 |
+
case "ADD_TOAST":
|
| 75 |
+
return {
|
| 76 |
+
...state,
|
| 77 |
+
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
| 78 |
+
};
|
| 79 |
+
|
| 80 |
+
case "UPDATE_TOAST":
|
| 81 |
+
return {
|
| 82 |
+
...state,
|
| 83 |
+
toasts: state.toasts.map((t) =>
|
| 84 |
+
t.id === action.toast.id ? { ...t, ...action.toast } : t,
|
| 85 |
+
),
|
| 86 |
+
};
|
| 87 |
+
|
| 88 |
+
case "DISMISS_TOAST": {
|
| 89 |
+
const { toastId } = action;
|
| 90 |
+
|
| 91 |
+
// ! Side effects ! - This could be extracted into a dismissToast() action,
|
| 92 |
+
// but I'll keep it here for simplicity
|
| 93 |
+
if (toastId) {
|
| 94 |
+
addToRemoveQueue(toastId);
|
| 95 |
+
} else {
|
| 96 |
+
state.toasts.forEach((toast) => {
|
| 97 |
+
addToRemoveQueue(toast.id);
|
| 98 |
+
});
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
return {
|
| 102 |
+
...state,
|
| 103 |
+
toasts: state.toasts.map((t) =>
|
| 104 |
+
t.id === toastId || toastId === undefined
|
| 105 |
+
? {
|
| 106 |
+
...t,
|
| 107 |
+
open: false,
|
| 108 |
+
}
|
| 109 |
+
: t,
|
| 110 |
+
),
|
| 111 |
+
};
|
| 112 |
+
}
|
| 113 |
+
case "REMOVE_TOAST":
|
| 114 |
+
if (action.toastId === undefined) {
|
| 115 |
+
return {
|
| 116 |
+
...state,
|
| 117 |
+
toasts: [],
|
| 118 |
+
};
|
| 119 |
+
}
|
| 120 |
+
return {
|
| 121 |
+
...state,
|
| 122 |
+
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
| 123 |
+
};
|
| 124 |
+
}
|
| 125 |
+
};
|
| 126 |
+
|
| 127 |
+
const listeners: Array<(state: State) => void> = [];
|
| 128 |
+
|
| 129 |
+
let memoryState: State = { toasts: [] };
|
| 130 |
+
|
| 131 |
+
function dispatch(action: Action) {
|
| 132 |
+
memoryState = reducer(memoryState, action);
|
| 133 |
+
listeners.forEach((listener) => {
|
| 134 |
+
listener(memoryState);
|
| 135 |
+
});
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
type Toast = Omit<ToasterToast, "id">;
|
| 139 |
+
|
| 140 |
+
function toast({ ...props }: Toast) {
|
| 141 |
+
const id = genId();
|
| 142 |
+
|
| 143 |
+
const update = (props: ToasterToast) =>
|
| 144 |
+
dispatch({
|
| 145 |
+
type: "UPDATE_TOAST",
|
| 146 |
+
toast: { ...props, id },
|
| 147 |
+
});
|
| 148 |
+
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
|
| 149 |
+
|
| 150 |
+
dispatch({
|
| 151 |
+
type: "ADD_TOAST",
|
| 152 |
+
toast: {
|
| 153 |
+
...props,
|
| 154 |
+
id,
|
| 155 |
+
open: true,
|
| 156 |
+
onOpenChange: (open) => {
|
| 157 |
+
if (!open) dismiss();
|
| 158 |
+
},
|
| 159 |
+
},
|
| 160 |
+
});
|
| 161 |
+
|
| 162 |
+
return {
|
| 163 |
+
id: id,
|
| 164 |
+
dismiss,
|
| 165 |
+
update,
|
| 166 |
+
};
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
function useToast() {
|
| 170 |
+
const [state, setState] = React.useState<State>(memoryState);
|
| 171 |
+
|
| 172 |
+
React.useEffect(() => {
|
| 173 |
+
listeners.push(setState);
|
| 174 |
+
return () => {
|
| 175 |
+
const index = listeners.indexOf(setState);
|
| 176 |
+
if (index > -1) {
|
| 177 |
+
listeners.splice(index, 1);
|
| 178 |
+
}
|
| 179 |
+
};
|
| 180 |
+
}, [state]);
|
| 181 |
+
|
| 182 |
+
return {
|
| 183 |
+
...state,
|
| 184 |
+
toast,
|
| 185 |
+
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
|
| 186 |
+
};
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
export { useToast, toast };
|
src/components/ui/utils.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { type ClassValue, clsx } from "clsx";
|
| 2 |
+
import { twMerge } from "tailwind-merge";
|
| 3 |
+
|
| 4 |
+
export function cn(...inputs: ClassValue[]) {
|
| 5 |
+
return twMerge(clsx(inputs));
|
| 6 |
+
}
|
src/util/presets.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export const presetArtStyles = [
|
| 2 |
+
{
|
| 3 |
+
artist: "Pixar",
|
| 4 |
+
prompt:
|
| 5 |
+
"in the style of Pixar, cartoon-style characters, 4k, 8k, unreal engine, octane render photorealistic by cosmicwonder, hdr, photography by cosmicwonder, high definition, symmetrical face, volumetric lighting, dusty haze, photo, octane render, 24mm, 4k, 24mm, DSLR, high quality, 60 fps, ultra realistic",
|
| 6 |
+
},
|
| 7 |
+
{
|
| 8 |
+
artist: "Minecraft",
|
| 9 |
+
prompt:
|
| 10 |
+
"in the style of Minecraft Character, minecraft, ultra hd, realistic, vivid colors, highly detailed, UHD drawing, pen and ink, perfect composition",
|
| 11 |
+
},
|
| 12 |
+
{
|
| 13 |
+
artist: "8 Bit pixel",
|
| 14 |
+
prompt:
|
| 15 |
+
"pixel style, pixel style, 8 bit pixel art, golden ratio, fake detail, trending pixiv fanbox, acrylic palette knife, style of makoto shinkai studio ghibli genshin impact james gilleard greg rutkowski chiho aoshima",
|
| 16 |
+
},
|
| 17 |
+
{
|
| 18 |
+
artist: "Vincent van Gogh",
|
| 19 |
+
prompt:
|
| 20 |
+
"in the style of Vincent van Gogh, with bold, expressive brush strokes and vibrant colors",
|
| 21 |
+
},
|
| 22 |
+
{
|
| 23 |
+
artist: "Claude Monet",
|
| 24 |
+
prompt:
|
| 25 |
+
"in the style of Claude Monet, using impressionist techniques with a focus on light and color",
|
| 26 |
+
},
|
| 27 |
+
{
|
| 28 |
+
artist: "Salvador Dalí",
|
| 29 |
+
prompt:
|
| 30 |
+
"in the style of Salvador Dalí, with dream-like, bizarre elements and melting clocks",
|
| 31 |
+
},
|
| 32 |
+
{
|
| 33 |
+
artist: "Pablo Picasso",
|
| 34 |
+
prompt:
|
| 35 |
+
"in the style of Pablo Picasso's Cubism, with geometric shapes and multiple perspectives merged into one image.",
|
| 36 |
+
},
|
| 37 |
+
{
|
| 38 |
+
artist: "Edvard Munch",
|
| 39 |
+
prompt:
|
| 40 |
+
"in the style of Edvard Munch, using intense colors and bold lines to convey strong emotions, such as in 'The Scream'.",
|
| 41 |
+
},
|
| 42 |
+
];
|
| 43 |
+
|
| 44 |
+
export const presetImage =
|
| 45 |
+
"data:image/jpeg;base64,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";
|
src/util/set-emoji-favicon.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
export const setEmojiFavicon = (emoji: string) => {
|
| 4 |
+
if (typeof document === "undefined") return;
|
| 5 |
+
const href = `data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>${emoji}</text></svg>`;
|
| 6 |
+
const link =
|
| 7 |
+
document.querySelector("link[rel*='icon']") ||
|
| 8 |
+
document.createElement("link");
|
| 9 |
+
link.setAttribute("rel", "icon");
|
| 10 |
+
link.setAttribute("href", href);
|
| 11 |
+
document.head.appendChild(link);
|
| 12 |
+
};
|
src/util/use-previous.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useEffect, useRef } from "react";
|
| 2 |
+
|
| 3 |
+
export function usePrevious<T>(value: T) {
|
| 4 |
+
const ref = useRef<T>();
|
| 5 |
+
useEffect(() => {
|
| 6 |
+
if (value) {
|
| 7 |
+
ref.current = value;
|
| 8 |
+
}
|
| 9 |
+
}, [value]);
|
| 10 |
+
return ref.current;
|
| 11 |
+
}
|
src/util/use-response.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import useSWR from "swr";
|
| 2 |
+
|
| 3 |
+
const dimension = 512;
|
| 4 |
+
function blobToBase64(blob: Blob): Promise<string> {
|
| 5 |
+
return new Promise((resolve, _) => {
|
| 6 |
+
const reader = new FileReader();
|
| 7 |
+
reader.onloadend = () => resolve(reader.result as string);
|
| 8 |
+
reader.readAsDataURL(blob);
|
| 9 |
+
});
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
function convertEmojiToDataToDataURL(emoji: string): string {
|
| 13 |
+
const element = document.createElement("canvas");
|
| 14 |
+
const ctx = element.getContext("2d")!;
|
| 15 |
+
element.height = dimension;
|
| 16 |
+
element.width = dimension;
|
| 17 |
+
ctx.fillStyle = "rgb(24 24 27)";
|
| 18 |
+
ctx.fillRect(0, 0, element.width, element.height);
|
| 19 |
+
ctx.textAlign = `center`;
|
| 20 |
+
ctx.font = `${dimension - 32}px serf`;
|
| 21 |
+
const textMetrics = ctx.measureText(emoji);
|
| 22 |
+
|
| 23 |
+
const textHeight =
|
| 24 |
+
textMetrics.actualBoundingBoxAscent + textMetrics.actualBoundingBoxDescent;
|
| 25 |
+
const y =
|
| 26 |
+
dimension / 2 + (textMetrics.actualBoundingBoxAscent - textHeight / 2);
|
| 27 |
+
|
| 28 |
+
ctx.fillText(emoji, dimension / 2, y);
|
| 29 |
+
return element.toDataURL("image/jpeg", 0.5);
|
| 30 |
+
}
|
| 31 |
+
export const useResponse = (
|
| 32 |
+
revalidateOnMount: boolean,
|
| 33 |
+
emoji: string,
|
| 34 |
+
name: string,
|
| 35 |
+
style: string,
|
| 36 |
+
strength: number,
|
| 37 |
+
seed: number,
|
| 38 |
+
) => {
|
| 39 |
+
const { data, isLoading } = useSWR(
|
| 40 |
+
[emoji, name, style, strength, seed],
|
| 41 |
+
async ([base64, name, style, strength, seed]) => {
|
| 42 |
+
const response = await fetch("/api/run", {
|
| 43 |
+
headers: {
|
| 44 |
+
accept: "image/jpeg",
|
| 45 |
+
"content-type": "application/json",
|
| 46 |
+
},
|
| 47 |
+
body: JSON.stringify({
|
| 48 |
+
input_image: convertEmojiToDataToDataURL(emoji).replace(
|
| 49 |
+
/^data:image\/(png|jpeg);base64,/,
|
| 50 |
+
"",
|
| 51 |
+
),
|
| 52 |
+
prompt: `${name}, emoji ${emoji}, ${style}`,
|
| 53 |
+
guidance_scale: 8,
|
| 54 |
+
lcm_steps: 50,
|
| 55 |
+
seed,
|
| 56 |
+
steps: 4,
|
| 57 |
+
strength,
|
| 58 |
+
width: dimension,
|
| 59 |
+
height: dimension,
|
| 60 |
+
}),
|
| 61 |
+
method: "POST",
|
| 62 |
+
});
|
| 63 |
+
if (response.status !== 200) return "";
|
| 64 |
+
const blob = await response.blob();
|
| 65 |
+
return await blobToBase64(blob);
|
| 66 |
+
},
|
| 67 |
+
{
|
| 68 |
+
revalidateOnFocus: false,
|
| 69 |
+
revalidateOnReconnect: false,
|
| 70 |
+
revalidateOnMount,
|
| 71 |
+
refreshWhenOffline: false,
|
| 72 |
+
refreshInterval: 0,
|
| 73 |
+
},
|
| 74 |
+
);
|
| 75 |
+
return { image: data as string, loading: isLoading };
|
| 76 |
+
};
|
src/util/use-share.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { presetArtStyles } from "@/util/presets";
|
| 2 |
+
import {
|
| 3 |
+
decompressFromEncodedURIComponent,
|
| 4 |
+
compressToEncodedURIComponent,
|
| 5 |
+
} from "lz-string";
|
| 6 |
+
import { useSearchParams } from "next/navigation";
|
| 7 |
+
|
| 8 |
+
export interface Option {
|
| 9 |
+
emoji: string;
|
| 10 |
+
name: string;
|
| 11 |
+
prompt: string;
|
| 12 |
+
seed: number;
|
| 13 |
+
strength: number;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
const fallbackOptions: Option = {
|
| 17 |
+
emoji: "🐤",
|
| 18 |
+
name: "cat",
|
| 19 |
+
prompt: presetArtStyles[0].prompt,
|
| 20 |
+
seed: 2159232,
|
| 21 |
+
strength: 0.7,
|
| 22 |
+
};
|
| 23 |
+
|
| 24 |
+
export const shareString2Json = (shareString: string): Option => {
|
| 25 |
+
try {
|
| 26 |
+
return JSON.parse(decompressFromEncodedURIComponent(shareString));
|
| 27 |
+
} catch (_) {
|
| 28 |
+
return fallbackOptions;
|
| 29 |
+
}
|
| 30 |
+
};
|
| 31 |
+
|
| 32 |
+
export const useShare = (): { option: Option; hasShare: boolean } => {
|
| 33 |
+
const searchParams = useSearchParams();
|
| 34 |
+
const shareParam = searchParams.get("share");
|
| 35 |
+
if (shareParam) {
|
| 36 |
+
try {
|
| 37 |
+
return {
|
| 38 |
+
option: JSON.parse(decompressFromEncodedURIComponent(shareParam)),
|
| 39 |
+
hasShare: true,
|
| 40 |
+
};
|
| 41 |
+
} catch (_) {
|
| 42 |
+
return { option: fallbackOptions, hasShare: false };
|
| 43 |
+
}
|
| 44 |
+
}
|
| 45 |
+
return { option: fallbackOptions, hasShare: false };
|
| 46 |
+
};
|
| 47 |
+
|
| 48 |
+
export const getShareUrl = (option: Option) => {
|
| 49 |
+
return compressToEncodedURIComponent(JSON.stringify(option));
|
| 50 |
+
};
|
style.css
CHANGED
|
@@ -1,28 +1,16 @@
|
|
| 1 |
-
body {
|
| 2 |
-
|
| 3 |
-
|
|
|
|
|
|
|
| 4 |
}
|
| 5 |
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
|
|
|
| 9 |
}
|
| 10 |
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
font-size: 15px;
|
| 14 |
-
margin-bottom: 10px;
|
| 15 |
-
margin-top: 5px;
|
| 16 |
-
}
|
| 17 |
-
|
| 18 |
-
.card {
|
| 19 |
-
max-width: 620px;
|
| 20 |
-
margin: 0 auto;
|
| 21 |
-
padding: 16px;
|
| 22 |
-
border: 1px solid lightgray;
|
| 23 |
-
border-radius: 16px;
|
| 24 |
-
}
|
| 25 |
-
|
| 26 |
-
.card p:last-child {
|
| 27 |
-
margin-bottom: 0;
|
| 28 |
}
|
|
|
|
| 1 |
+
html, body {
|
| 2 |
+
padding: 0;
|
| 3 |
+
margin: 0;
|
| 4 |
+
height: 100%;
|
| 5 |
+
width: 100%;
|
| 6 |
}
|
| 7 |
|
| 8 |
+
iframe {
|
| 9 |
+
border: none;
|
| 10 |
+
height: 100%;
|
| 11 |
+
width: 100%;
|
| 12 |
}
|
| 13 |
|
| 14 |
+
* {
|
| 15 |
+
outline: none;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
}
|
tailwind.config.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { Config } from "tailwindcss";
|
| 2 |
+
|
| 3 |
+
const config: Config = {
|
| 4 |
+
content: [
|
| 5 |
+
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
|
| 6 |
+
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
|
| 7 |
+
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
|
| 8 |
+
],
|
| 9 |
+
theme: {
|
| 10 |
+
extend: {
|
| 11 |
+
backgroundImage: {
|
| 12 |
+
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
|
| 13 |
+
"gradient-conic":
|
| 14 |
+
"conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
|
| 15 |
+
},
|
| 16 |
+
keyframes: {
|
| 17 |
+
shake: {
|
| 18 |
+
"10%, 90%": {
|
| 19 |
+
transform: "translate3d(0, 1px, 0)",
|
| 20 |
+
},
|
| 21 |
+
"20%, 80%": {
|
| 22 |
+
transform: "translate3d(0, -2px, 0)",
|
| 23 |
+
},
|
| 24 |
+
"30%, 50%, 70%": {
|
| 25 |
+
transform: "translate3d(0, 4px, 0)",
|
| 26 |
+
},
|
| 27 |
+
"40%, 60%": {
|
| 28 |
+
transform: "translate3d(0, -4px, 0)",
|
| 29 |
+
},
|
| 30 |
+
},
|
| 31 |
+
},
|
| 32 |
+
},
|
| 33 |
+
},
|
| 34 |
+
plugins: [],
|
| 35 |
+
};
|
| 36 |
+
export default config;
|
tsconfig.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"target": "es5",
|
| 4 |
+
"lib": ["dom", "dom.iterable", "esnext"],
|
| 5 |
+
"allowJs": true,
|
| 6 |
+
"skipLibCheck": true,
|
| 7 |
+
"strict": true,
|
| 8 |
+
"noEmit": true,
|
| 9 |
+
"esModuleInterop": true,
|
| 10 |
+
"module": "esnext",
|
| 11 |
+
"moduleResolution": "bundler",
|
| 12 |
+
"resolveJsonModule": true,
|
| 13 |
+
"isolatedModules": true,
|
| 14 |
+
"jsx": "preserve",
|
| 15 |
+
"incremental": true,
|
| 16 |
+
"plugins": [
|
| 17 |
+
{
|
| 18 |
+
"name": "next"
|
| 19 |
+
}
|
| 20 |
+
],
|
| 21 |
+
"paths": {
|
| 22 |
+
"@/*": ["./src/*"]
|
| 23 |
+
}
|
| 24 |
+
},
|
| 25 |
+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
| 26 |
+
"exclude": ["node_modules"]
|
| 27 |
+
}
|