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 = "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"
|
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 |
+
}
|