Skip to content

Commit 969c838

Browse files
authoredMar 7, 2024··
Merge pull request #13 from line/dev
V3
2 parents 265d127 + 6c34e9f commit 969c838

38 files changed

+863
-300
lines changed
 

‎.github/workflows/deploy.yml

+29-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ on:
1010
workflow_dispatch:
1111

1212
permissions:
13-
contents: read
13+
contents: write
1414
pages: write
1515
id-token: write
1616

@@ -45,3 +45,31 @@ jobs:
4545
- name: Deploy to GitHub Pages
4646
id: deployment
4747
uses: actions/deploy-pages@v2
48+
49+
# Tagging job
50+
tag:
51+
runs-on: ubuntu-latest
52+
steps:
53+
- name: Checkout repository
54+
uses: actions/checkout@v2
55+
with:
56+
fetch-depth: "0"
57+
- name: Read packages.json
58+
run: |
59+
echo "PACKAGE_JSON=$(jq -c . < package.json)" >> $GITHUB_ENV
60+
- name: Generate tag version (headver)
61+
run: |
62+
VERSION_PREFIX="v"
63+
VERSION_HEAD=$(cut -d '.' -f 1 <<< ${{ fromJson(env.PACKAGE_JSON).version }})
64+
VERSION_YEAR=$(date +%g)
65+
VERSION_WEEK=$(date +%V)
66+
VERSION_BUILD=${{github.run_number}}
67+
NEW_TAG="${VERSION_PREFIX}${VERSION_HEAD}.${VERSION_YEAR}${VERSION_WEEK}.${VERSION_BUILD}"
68+
echo "Generated new tag: $NEW_TAG"
69+
echo "NEW_TAG=$NEW_TAG" >> $GITHUB_ENV
70+
- name: Push Git Tag
71+
run: |
72+
git config user.name "GitHub Actions"
73+
git config user.email "github-actions@users.noreply.github.com"
74+
git tag $NEW_TAG
75+
git push origin $NEW_TAG

‎README.md

+4-2
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ The `app.config.json` should be as below;
129129
| backgrounds | Array of images. (Detailed explanation about `Image` is below) | |
130130
| inputFields (optional) | Set up an input field group just for this theme | |
131131
| isNew (optional) | If it's true, new icon shows up next to this theme | `false` |
132+
| isHidden (optional) | If it's true, this theme will be hidden | `false` |
132133

133134
### 🌆 Background Images
134135

@@ -171,6 +172,7 @@ The `app.config.json` should be as below;
171172
| offset | Adjusting detail position from origin point. (Detailed explanation is below) | |
172173
| isRequired (optional) | | `false` |
173174
| text (optional) | Default value of input | `""` |
175+
| tooltip (optional) | Allows you to add a description of the input. | `""` |
174176

175177
- Offset
176178

@@ -282,7 +284,7 @@ With above structure, let say you want to override `themes` node for `office` th
282284
"fields": [
283285
{
284286
"label": "name",
285-
"fontSize": "medium",
287+
"fontSize": "Large",
286288
"fontStyle": "LINE Seed",
287289
"offset": {
288290
"x": "0%",
@@ -297,7 +299,7 @@ With above structure, let say you want to override `themes` node for `office` th
297299
"fields": [
298300
{
299301
"label": "Team Name",
300-
"fontSize": "medium",
302+
"fontSize": "Large",
301303
"fontStyle": "LINE Seed",
302304
"offset": {
303305
"x": "0%",

‎app.config.json

+18-16
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,14 @@
1111
"type": "filesystem",
1212
"path": "backgrounds"
1313
},
14+
"contributeGuide": "./CONTRIBUTING.md",
1415
"fonts": {
1516
"sizes": {
16-
"large": "6rem",
17-
"medium": "4.5rem",
18-
"regular": "3.5rem",
19-
"small": "3rem",
20-
"caption": "1.5rem"
17+
"XLarge": "6rem",
18+
"Large": "4.5rem",
19+
"Base": "3.5rem",
20+
"Small": "3rem",
21+
"XSmall": "1.5rem"
2122
},
2223
"styles": ["LINE Seed"]
2324
},
@@ -28,17 +29,18 @@
2829
"fields": [
2930
{
3031
"label": "name",
31-
"fontSize": "medium",
32+
"fontSize": "Large",
3233
"fontStyle": "LINE Seed",
3334
"offset": {
3435
"x": "0%",
3536
"y": "0%"
3637
},
37-
"isRequired": true
38+
"isRequired": true,
39+
"tooltip": "Please enter your name."
3840
},
3941
{
4042
"label": "team",
41-
"fontSize": "small",
43+
"fontSize": "Small",
4244
"fontStyle": "LINE Seed",
4345
"offset": {
4446
"x": "0%",
@@ -47,7 +49,7 @@
4749
},
4850
{
4951
"label": "role",
50-
"fontSize": "small",
52+
"fontSize": "Small",
5153
"fontStyle": "LINE Seed",
5254
"offset": {
5355
"x": "0%",
@@ -70,7 +72,7 @@
7072
"fields": [
7173
{
7274
"label": "name",
73-
"fontSize": "medium",
75+
"fontSize": "Large",
7476
"fontStyle": "LINE Seed",
7577
"offset": {
7678
"x": "0%",
@@ -80,7 +82,7 @@
8082
},
8183
{
8284
"label": "team",
83-
"fontSize": "small",
85+
"fontSize": "Small",
8486
"fontStyle": "LINE Seed",
8587
"offset": {
8688
"x": "0%",
@@ -89,7 +91,7 @@
8991
},
9092
{
9193
"label": "role",
92-
"fontSize": "small",
94+
"fontSize": "Small",
9395
"fontStyle": "LINE Seed",
9496
"offset": {
9597
"x": "0%",
@@ -103,7 +105,7 @@
103105
"fields": [
104106
{
105107
"label": "Company",
106-
"fontSize": "medium",
108+
"fontSize": "Large",
107109
"fontStyle": "LINE Seed",
108110
"offset": {
109111
"x": "0%",
@@ -124,7 +126,7 @@
124126
"fields": [
125127
{
126128
"label": "name",
127-
"fontSize": "medium",
129+
"fontSize": "Large",
128130
"fontStyle": "LINE Seed",
129131
"offset": {
130132
"x": "0%",
@@ -134,7 +136,7 @@
134136
},
135137
{
136138
"label": "role",
137-
"fontSize": "small",
139+
"fontSize": "Small",
138140
"fontStyle": "LINE Seed",
139141
"offset": {
140142
"x": "0%",
@@ -148,7 +150,7 @@
148150
"fields": [
149151
{
150152
"label": "Company",
151-
"fontSize": "medium",
153+
"fontSize": "Large",
152154
"fontStyle": "LINE Seed",
153155
"offset": {
154156
"x": "0%",

‎package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "abc-virtual-background-maker",
33
"private": true,
4-
"version": "2.1.0",
4+
"version": "3.0.0",
55
"type": "module",
66
"scripts": {
77
"dev": "vite",

‎src/App.tsx

+5-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* under the License.
1515
*/
1616
import { CSSProperties, useEffect, useRef } from "react";
17-
import { keyColor, title } from "~/output.config.json";
17+
import config from "~/output.config.json";
1818

1919
import {
2020
DownloadButton,
@@ -23,10 +23,13 @@ import {
2323
SyncButton,
2424
ThemeMenu,
2525
} from "@/components";
26+
import { Config } from "@/constants/config";
2627
import { AppProvider, useMediaQuery, useSnapshot, useTitle } from "@/hooks";
28+
import locales from "@/locales/en-US.json";
2729
import styles from "./App.module.scss";
2830

2931
function App() {
32+
const { keyColor, title } = config as unknown as Config;
3033
const imageAreaRef = useRef<HTMLDivElement>(null);
3134
const { saveImage, loading } = useSnapshot(imageAreaRef);
3235
const { logo, text } = title;
@@ -62,7 +65,7 @@ function App() {
6265
<div className={styles.content}>
6366
<nav className={styles.navigation}>
6467
<h2>
65-
Select Theme <SyncButton />
68+
{locales["title"]["theme"]} <SyncButton />
6669
</h2>
6770
<div>
6871
<ThemeMenu />
+103
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/**
2+
* Copyright 2023 LINE Corporation
3+
*
4+
* LINE Corporation licenses this file to you under the Apache License,
5+
* version 2.0 (the "License"); you may not use this file except in compliance
6+
* with the License. You may obtain a copy of the License at:
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations
14+
* under the License.
15+
*/
16+
17+
.dim {
18+
position: fixed;
19+
top: 0;
20+
left: 0;
21+
z-index: 100;
22+
display: flex;
23+
align-items: center;
24+
justify-content: center;
25+
width: 100%;
26+
height: 100%;
27+
background-color: var(--background-dim);
28+
animation: fade-in 0.3s cubic-bezier(0.17, 0.55, 0.55, 1);
29+
}
30+
31+
.modal {
32+
padding: 1rem;
33+
display: flex;
34+
flex-direction: column;
35+
align-items: center;
36+
justify-content: center;
37+
max-width: min(45rem, calc(100vw - 3rem));
38+
background-color: var(--background-tertiary);
39+
white-space: pre-line;
40+
text-align: center;
41+
border-radius: 0.5rem;
42+
animation:
43+
fade-in 0.3s cubic-bezier(0.17, 0.55, 0.55, 1),
44+
scale-up 0.3s cubic-bezier(0.17, 0.55, 0.55, 1);
45+
46+
:global {
47+
.material-symbols-outlined {
48+
margin-bottom: 0.5rem;
49+
color: var(--red-primary);
50+
font-size: 2rem;
51+
font-variation-settings:
52+
"FILL" 1,
53+
"wght" 400,
54+
"GRAD" 0,
55+
"opsz" 24;
56+
}
57+
}
58+
59+
strong {
60+
@include font("16", "label-primary");
61+
62+
font-weight: bold;
63+
}
64+
65+
p {
66+
@include font("14", "label-secondary");
67+
68+
margin-top: 0.5rem;
69+
}
70+
}
71+
72+
.button {
73+
@include font("16", "label-above-primary");
74+
75+
margin-top: 1rem;
76+
width: 100%;
77+
height: 3rem;
78+
display: flex;
79+
align-items: center;
80+
justify-content: center;
81+
background-color: var(--red-primary);
82+
border-radius: 0.5rem;
83+
}
84+
85+
@keyframes fade-in {
86+
from {
87+
opacity: 0;
88+
}
89+
90+
to {
91+
opacity: 1;
92+
}
93+
}
94+
95+
@keyframes scale-up {
96+
from {
97+
transform: scale(0.85);
98+
}
99+
100+
to {
101+
transform: scale(1);
102+
}
103+
}

‎src/components/Alert/Alert.tsx

+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/**
2+
* Copyright 2023 LINE Corporation
3+
*
4+
* LINE Corporation licenses this file to you under the Apache License,
5+
* version 2.0 (the "License"); you may not use this file except in compliance
6+
* with the License. You may obtain a copy of the License at:
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations
14+
* under the License.
15+
*/
16+
17+
import type {
18+
ButtonHTMLAttributes,
19+
KeyboardEvent,
20+
PropsWithChildren,
21+
} from "react";
22+
import { useCallback, useEffect, useRef } from "react";
23+
import ReactDOM from "react-dom";
24+
25+
import styles from "./Alert.module.scss";
26+
27+
interface Props extends PropsWithChildren {
28+
onClose: VoidFunction;
29+
}
30+
31+
const Alert = (props: Props) => {
32+
const { onClose, children } = props;
33+
34+
const dialogRoot = document.getElementsByTagName("body")[0];
35+
const dialogRef = useRef<HTMLDivElement>(document.createElement("div"));
36+
const dialogRefCurrent = dialogRef.current;
37+
38+
const handleKeyDown = useCallback(
39+
(event: KeyboardEvent<HTMLDivElement>) => {
40+
if (event.key === "Escape") {
41+
onClose();
42+
}
43+
},
44+
[onClose],
45+
);
46+
47+
useEffect(() => {
48+
if (dialogRefCurrent && dialogRoot) {
49+
dialogRoot.appendChild(dialogRefCurrent);
50+
51+
return () => {
52+
dialogRoot.removeChild(dialogRefCurrent);
53+
};
54+
}
55+
}, [dialogRefCurrent, dialogRoot]);
56+
57+
return ReactDOM.createPortal(
58+
<div
59+
className={styles.dim}
60+
onClick={onClose}
61+
onKeyDown={handleKeyDown}
62+
role="presentation"
63+
>
64+
<div
65+
className={styles.modal}
66+
role="dialog"
67+
onClick={(e) => e.stopPropagation()}
68+
onKeyDown={handleKeyDown}
69+
>
70+
<span className="material-symbols-outlined">error</span>
71+
{children}
72+
</div>
73+
</div>,
74+
dialogRefCurrent,
75+
);
76+
};
77+
78+
export default Alert;
79+
80+
export const AlertButton = ({
81+
onClick,
82+
children,
83+
}: ButtonHTMLAttributes<HTMLButtonElement>) => (
84+
<button className={styles.button} onClick={onClick}>
85+
{children}
86+
</button>
87+
);

‎src/components/Alert/index.ts

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/**
2+
* Copyright 2023 LINE Corporation
3+
*
4+
* LINE Corporation licenses this file to you under the Apache License,
5+
* version 2.0 (the "License"); you may not use this file except in compliance
6+
* with the License. You may obtain a copy of the License at:
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations
14+
* under the License.
15+
*/
16+
17+
export { default as Alert, AlertButton } from "./Alert";

‎src/components/DownloadButton/DownloadButton.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import { ButtonHTMLAttributes } from "react";
1717

1818
import { Loading } from "@/components";
19+
import locales from "@/locales/en-US.json";
1920
import styles from "./DownloadButton.module.scss";
2021

2122
interface Props
@@ -43,7 +44,7 @@ const DownloadButton = (props: Props) => {
4344
<span className="material-symbols-outlined">download</span>
4445
)}
4546
</span>
46-
<span className={styles.text}>Download</span>
47+
<span className={styles.text}>{locales["button"]["download"]}</span>
4748
</button>
4849
);
4950
};

‎src/components/DragAndDropFile/DragAndDropFile.module.scss

+15-12
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@
2929
align-items: center;
3030
justify-content: center;
3131
cursor: pointer;
32+
overflow: hidden;
33+
border-radius: 0.5rem;
34+
box-shadow: 0 0 0.5px 0.5px var(--fill-tertiary);
3235

3336
.dragging {
3437
z-index: 2;
@@ -44,18 +47,6 @@
4447
animation: scale 0.3s;
4548
}
4649

47-
@keyframes scale {
48-
from {
49-
opacity: 0;
50-
transform: scale(0.8);
51-
}
52-
53-
to {
54-
opacity: 1;
55-
transform: scale(1);
56-
}
57-
}
58-
5950
.icon {
6051
font-size: 3rem;
6152
}
@@ -69,3 +60,15 @@
6960
opacity: 0;
7061
}
7162
}
63+
64+
@keyframes scale {
65+
from {
66+
opacity: 0;
67+
transform: scale(0.8);
68+
}
69+
70+
to {
71+
opacity: 1;
72+
transform: scale(1);
73+
}
74+
}

‎src/components/DragAndDropFile/DragAndDropFile.tsx

+20-57
Original file line numberDiff line numberDiff line change
@@ -14,72 +14,26 @@
1414
* under the License.
1515
*/
1616

17-
import { useCallback, useEffect, useRef, useState } from "react";
17+
import { useRef, useState } from "react";
1818

19-
import { useAppConfiguration } from "@/hooks";
19+
import { Alert, AlertButton } from "@/components";
20+
import { useAppConfiguration, useDragAndDrop } from "@/hooks";
21+
import locales from "@/locales/en-US.json";
2022
import styles from "./DragAndDropFile.module.scss";
2123

2224
const DragAndDropFile = () => {
2325
const { handleChangeCustomImages, handleDropCustomImages } =
2426
useAppConfiguration();
2527
const dragRef = useRef<HTMLInputElement>(null);
26-
const [isDragging, setIsDragging] = useState(false);
27-
28-
const handleDragIn = useCallback((e: DragEvent): void => {
29-
e.preventDefault();
30-
e.stopPropagation();
31-
}, []);
32-
33-
const handleDragOut = useCallback((e: DragEvent): void => {
34-
e.preventDefault();
35-
e.stopPropagation();
36-
37-
setIsDragging(false);
38-
}, []);
39-
40-
const handleDragOver = useCallback((e: DragEvent): void => {
41-
e.preventDefault();
42-
e.stopPropagation();
43-
44-
if (e.dataTransfer!.files) {
45-
setIsDragging(true);
46-
}
47-
}, []);
48-
49-
const handleDrop = useCallback(
50-
(e: DragEvent) => {
51-
e.preventDefault();
52-
e.stopPropagation();
53-
28+
const [showAlert, setShowAlert] = useState(false);
29+
const handleDrop = (e: DragEvent) => {
30+
try {
5431
handleDropCustomImages(e);
55-
setIsDragging(false);
56-
},
57-
[handleDropCustomImages],
58-
);
59-
60-
const initDragEvents = useCallback(() => {
61-
if (dragRef.current !== null) {
62-
dragRef.current.addEventListener("dragenter", handleDragIn);
63-
dragRef.current.addEventListener("dragleave", handleDragOut);
64-
dragRef.current.addEventListener("dragover", handleDragOver);
65-
dragRef.current.addEventListener("drop", handleDrop);
32+
} catch (e) {
33+
setShowAlert(true);
6634
}
67-
}, [handleDragIn, handleDragOut, handleDragOver, handleDrop]);
68-
69-
const resetDragEvents = useCallback(() => {
70-
if (dragRef.current !== null) {
71-
dragRef.current.removeEventListener("dragenter", handleDragIn);
72-
dragRef.current.removeEventListener("dragleave", handleDragOut);
73-
dragRef.current.removeEventListener("dragover", handleDragOver);
74-
dragRef.current.removeEventListener("drop", handleDrop);
75-
}
76-
}, [handleDragIn, handleDragOut, handleDragOver, handleDrop]);
77-
78-
useEffect(() => {
79-
initDragEvents();
80-
81-
return () => resetDragEvents();
82-
}, [initDragEvents, resetDragEvents]);
35+
};
36+
const { isDragging } = useDragAndDrop(dragRef, handleDrop);
8337

8438
return (
8539
<label className={styles.file}>
@@ -94,6 +48,15 @@ const DragAndDropFile = () => {
9448
accept="image/*"
9549
ref={dragRef}
9650
/>
51+
{showAlert && (
52+
<Alert onClose={() => setShowAlert(false)}>
53+
<strong>{locales["alert"]["uploadOnlyImages"]}</strong>
54+
<p>{locales["alert"]["makeSureImageExtensions"]}</p>
55+
<AlertButton onClick={() => setShowAlert(false)}>
56+
{locales["button"]["confirm"]}
57+
</AlertButton>
58+
</Alert>
59+
)}
9760
</label>
9861
);
9962
};

‎src/components/EditableImage/EditableImage.module.scss

+39-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,29 @@
2222
transform: scaleX(-1);
2323
}
2424

25-
> img {
25+
.drop {
26+
position: absolute;
27+
top: 0;
28+
left: 0;
29+
width: 100%;
30+
height: 100%;
31+
32+
.dragging {
33+
z-index: 2;
34+
position: absolute;
35+
top: 0.5rem;
36+
left: 0.5rem;
37+
width: calc(100% - 1rem);
38+
height: calc(100% - 1rem);
39+
border: 1px dashed var(--red-primary);
40+
border-radius: 0.5rem;
41+
background-color: rgb(0 0 0 / 10%);
42+
pointer-events: none;
43+
animation: scale 0.3s;
44+
}
45+
}
46+
47+
img {
2648
position: absolute;
2749
top: 0;
2850
left: 0;
@@ -31,6 +53,9 @@
3153
height: 100%;
3254
object-fit: cover;
3355
-webkit-user-drag: none;
56+
overflow: hidden;
57+
border-radius: 0.5rem;
58+
box-shadow: 0 0 0.5px 0.5px var(--fill-tertiary);
3459
}
3560

3661
.inputs {
@@ -101,10 +126,23 @@
101126

102127
> li {
103128
margin: 0 0.5rem;
129+
position: relative;
104130

105131
&:first-of-type {
106132
margin: 0 0.5rem 0.5rem;
107133
}
108134
}
109135
}
110136
}
137+
138+
@keyframes scale {
139+
from {
140+
opacity: 0;
141+
transform: scale(0.8);
142+
}
143+
144+
to {
145+
opacity: 1;
146+
transform: scale(1);
147+
}
148+
}

‎src/components/EditableImage/EditableImage.tsx

+40-5
Original file line numberDiff line numberDiff line change
@@ -19,30 +19,37 @@ import {
1919
forwardRef,
2020
MutableRefObject,
2121
useEffect,
22+
useRef,
2223
useState,
2324
} from "react";
2425

25-
import { DragAndDropFile, TextInput } from "@/components";
26+
import { Alert, AlertButton, DragAndDropFile, TextInput } from "@/components";
2627
import {
2728
Alignment,
2829
Font,
2930
FontSize,
3031
InputField,
3132
InputFieldGroup,
3233
} from "@/constants";
33-
import { useAppConfiguration, useElementSize, useImageColor } from "@/hooks";
34+
import {
35+
useAppConfiguration,
36+
useDragAndDrop,
37+
useElementSize,
38+
useImageColor,
39+
} from "@/hooks";
40+
import locales from "@/locales/en-US.json";
3441
import { convertHexToRgb } from "@/utils";
3542
import styles from "./EditableImage.module.scss";
3643

3744
interface Props {
3845
dragKey?: string;
39-
selectedTheme?: string;
4046
src: string;
4147
fontColor?: string;
4248
inputRefs?: MutableRefObject<Record<string, HTMLInputElement>>;
4349
inputFields: Array<InputFieldGroup>;
4450
inputOptions: Record<string, InputField>;
4551
isEditable?: boolean;
52+
isImageDroppable?: boolean;
4653
isImageFlip?: boolean;
4754
focusedInput?: string;
4855
handleFocusInput?: (event: FocusEvent<HTMLDivElement>) => void;
@@ -59,14 +66,25 @@ const EditableImage = forwardRef<HTMLDivElement, Props>((props, ref) => {
5966
inputOptions,
6067
isEditable,
6168
isImageFlip,
69+
isImageDroppable,
6270
focusedInput,
6371
handleFocusInput,
6472
handleBlurInput,
6573
} = props;
6674
const { isDarkImage } = useImageColor(src);
6775
const [imageRef, { width: imageWidth }] = useElementSize();
6876
const [initialColor, setInitialColor] = useState("");
69-
const { defaultInputFields } = useAppConfiguration();
77+
const { defaultInputFields, handleDropCustomImages } = useAppConfiguration();
78+
const [showAlert, setShowAlert] = useState(false);
79+
const dragRef = useRef<HTMLDivElement>(null);
80+
const handleDrop = (e: DragEvent) => {
81+
try {
82+
handleDropCustomImages(e);
83+
} catch (e) {
84+
setShowAlert(true);
85+
}
86+
};
87+
const { isDragging } = useDragAndDrop(dragRef, handleDrop);
7088

7189
useEffect(() => {
7290
setInitialColor(
@@ -113,7 +131,14 @@ const EditableImage = forwardRef<HTMLDivElement, Props>((props, ref) => {
113131
}
114132
>
115133
{src ? (
116-
<img src={src} alt="" ref={imageRef} width={1152} height={648} />
134+
isImageDroppable ? (
135+
<div ref={dragRef} className={styles.drop}>
136+
{isDragging && <span className={styles.dragging}></span>}
137+
<img src={src} alt="" ref={imageRef} width={1152} height={648} />
138+
</div>
139+
) : (
140+
<img src={src} alt="" ref={imageRef} width={1152} height={648} />
141+
)
117142
) : (
118143
<DragAndDropFile />
119144
)}
@@ -179,6 +204,7 @@ const EditableImage = forwardRef<HTMLDivElement, Props>((props, ref) => {
179204
alignment={inputOptions[label].alignment as Alignment}
180205
label={label}
181206
defaultValue={text ?? ""}
207+
tooltip={inputOptions[label]?.tooltip}
182208
isFocused={focusedInput === label}
183209
onFocus={handleFocusInput}
184210
onBlur={handleBlurInput}
@@ -191,6 +217,15 @@ const EditableImage = forwardRef<HTMLDivElement, Props>((props, ref) => {
191217
</ul>
192218
);
193219
})}
220+
{showAlert && (
221+
<Alert onClose={() => setShowAlert(false)}>
222+
<strong>{locales["alert"]["uploadOnlyImages"]}</strong>
223+
<p>{locales["alert"]["makeSureImageExtensions"]}</p>
224+
<AlertButton onClick={() => setShowAlert(false)}>
225+
{locales["button"]["confirm"]}
226+
</AlertButton>
227+
</Alert>
228+
)}
194229
</div>
195230
);
196231
});

‎src/components/Editor/Editor.module.scss

+3-100
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,7 @@
2323
}
2424

2525
.inner {
26-
overflow: hidden;
27-
border-radius: 0.5rem;
28-
box-shadow: 0 0 0.5px 0.5px var(--fill-tertiary);
26+
overflow: visible;
2927
}
3028

3129
.buttons {
@@ -49,108 +47,13 @@
4947
}
5048
}
5149

52-
.image {
53-
position: relative;
54-
padding-top: 56.25%;
55-
56-
&.flip {
57-
transform: scaleX(-1);
58-
}
59-
60-
> img {
61-
position: absolute;
62-
top: 0;
63-
left: 0;
64-
z-index: 1;
65-
width: 100%;
66-
height: 100%;
67-
object-fit: cover;
68-
-webkit-user-drag: none;
69-
}
70-
71-
.inputs {
72-
position: absolute;
73-
z-index: 2;
74-
display: flex;
75-
padding: 2.5rem 2.3rem;
76-
transform: scale(var(--text-scale));
77-
78-
&.top-left {
79-
top: 0;
80-
left: 0;
81-
transform-origin: top left;
82-
}
83-
84-
&.top-center {
85-
top: 0;
86-
left: 50%;
87-
transform: scale(var(--text-scale)) translateX(-50%);
88-
transform-origin: top left;
89-
}
90-
91-
&.top-right {
92-
top: 0;
93-
right: 0;
94-
transform-origin: top right;
95-
}
96-
97-
&.center-left {
98-
top: 50%;
99-
left: 0;
100-
transform: scale(var(--text-scale)) translateY(-50%);
101-
transform-origin: top left;
102-
}
103-
104-
&.center {
105-
top: 50%;
106-
left: 50%;
107-
transform: scale(var(--text-scale)) translate(-50%, -50%);
108-
transform-origin: top left;
109-
}
110-
111-
&.center-right {
112-
top: 50%;
113-
right: 0;
114-
transform: scale(var(--text-scale)) translateY(-50%);
115-
transform-origin: top right;
116-
}
117-
118-
&.bottom-left {
119-
bottom: 0;
120-
left: 0;
121-
transform-origin: bottom left;
122-
}
123-
124-
&.bottom-center {
125-
bottom: 0;
126-
left: 50%;
127-
transform: scale(var(--text-scale)) translateX(-50%);
128-
transform-origin: bottom left;
129-
}
130-
131-
&.bottom-right {
132-
bottom: 0;
133-
right: 0;
134-
transform-origin: bottom right;
135-
}
136-
137-
> li {
138-
margin: 0 0.5rem;
139-
140-
&:first-of-type {
141-
margin: 0 0.5rem 0.5rem;
142-
}
143-
}
144-
}
145-
}
146-
14750
.side {
14851
width: 22.5rem;
149-
padding: 1.5rem;
52+
padding: 1.5rem 1.5rem 1.5rem 0.5rem;
15053

15154
@include mobile {
15255
width: 100%;
153-
padding-top: 0;
56+
padding: 0 1.5rem 1.5rem;
15457
}
15558

15659
h3 {

‎src/components/Editor/Editor.tsx

+18-12
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import {
4545
} from "@/constants";
4646
import { useAppConfiguration, useImageColor } from "@/hooks";
4747
import { IconGithub } from "@/icons";
48+
import locales from "@/locales/en-US.json";
4849
import { convertRgbToHex } from "@/utils";
4950
import styles from "./Editor.module.scss";
5051

@@ -115,12 +116,16 @@ const Editor = forwardRef<HTMLDivElement>((_, ref) => {
115116
const initialOptions: Record<string, InputField> = {};
116117
inputFields.map(({ fields }) => {
117118
fields.map(
118-
({ label, fontSize, fontStyle = DEFAULT_FONT_STYLE, text }, index) => {
119+
(
120+
{ label, fontSize, fontStyle = DEFAULT_FONT_STYLE, text, tooltip },
121+
index,
122+
) => {
119123
initialOptions[label] = {
120124
label,
121125
fontStyle,
122126
fontSize,
123127
text,
128+
tooltip,
124129
isVisible: true,
125130
opacity: index === 0 ? 100 : 60,
126131
};
@@ -222,6 +227,7 @@ const Editor = forwardRef<HTMLDivElement>((_, ref) => {
222227
dragKey={dragKey}
223228
ref={ref}
224229
isEditable
230+
isImageDroppable={selectedTheme === "custom"}
225231
isImageFlip={isImageFlip}
226232
src={src}
227233
fontColor={fontColor}
@@ -238,10 +244,10 @@ const Editor = forwardRef<HTMLDivElement>((_, ref) => {
238244
icon={<span className="material-symbols-outlined">help</span>}
239245
onClick={() => setGuideVisible(true)}
240246
>
241-
How To Contribute
247+
{locales["button"]["contribute"]}
242248
</TextButton>
243249
<TextButton icon={<IconGithub />} onClick={handleClickGithub}>
244-
Github
250+
{locales["button"]["github"]}
245251
</TextButton>
246252
</div>
247253
</div>
@@ -255,7 +261,7 @@ const Editor = forwardRef<HTMLDivElement>((_, ref) => {
255261
tabIndex={0}
256262
>
257263
<li className={styles.custom}>
258-
<strong>Font</strong>
264+
<strong>{locales["option"]["font"]}</strong>
259265
<Select
260266
defaultValue={inputOptions?.[focusedInput]?.fontStyle}
261267
value={inputOptions?.[focusedInput]?.fontStyle}
@@ -267,7 +273,7 @@ const Editor = forwardRef<HTMLDivElement>((_, ref) => {
267273
/>
268274
</li>
269275
<li className={styles.custom}>
270-
<strong>Size</strong>
276+
<strong>{locales["option"]["size"]}</strong>
271277
<Select
272278
defaultValue={inputOptions?.[focusedInput]?.fontSize}
273279
value={inputOptions?.[focusedInput]?.fontSize}
@@ -281,7 +287,7 @@ const Editor = forwardRef<HTMLDivElement>((_, ref) => {
281287
/>
282288
</li>
283289
<li className={styles.custom}>
284-
<strong>Color</strong>
290+
<strong>{locales["option"]["color"]}</strong>
285291
<ColorPicker
286292
defaultValue={
287293
inputOptions?.[focusedInput]?.fontColor ??
@@ -291,14 +297,14 @@ const Editor = forwardRef<HTMLDivElement>((_, ref) => {
291297
/>
292298
</li>
293299
<li className={styles.custom}>
294-
<strong>Opacity</strong>
300+
<strong>{locales["option"]["opacity"]}</strong>
295301
<OpacityRange
296302
defaultValue={inputOptions?.[focusedInput]?.opacity}
297303
onChange={(e) => handleChangeOpacity(Number(e.target.value))}
298304
/>
299305
</li>
300306
<li className={styles.custom}>
301-
<strong>Alignment</strong>
307+
<strong>{locales["option"]["alignment"]}</strong>
302308
<AlignSelect
303309
defaultValue={
304310
inputOptions?.[focusedInput]?.alignment ?? "left"
@@ -319,7 +325,7 @@ const Editor = forwardRef<HTMLDivElement>((_, ref) => {
319325
</>
320326
) : (
321327
<>
322-
<h3>Text Option</h3>
328+
<h3>{locales["option"]["text"]}</h3>
323329
{inputFields.map(({ fields }, index) => (
324330
<ul key={index}>
325331
{fields.map(({ label, isRequired }, index) => (
@@ -334,14 +340,14 @@ const Editor = forwardRef<HTMLDivElement>((_, ref) => {
334340
))}
335341
</ul>
336342
))}
337-
<h3>Image Option</h3>
343+
<h3>{locales["option"]["image"]}</h3>
338344
<ul>
339345
<li>
340346
<ToggleOption
341347
label={
342348
<>
343349
<span className="material-symbols-outlined">flip</span>
344-
Horizontal Flip
350+
{locales["option"]["flip"]}
345351
</>
346352
}
347353
isSelected={isImageFlip}
@@ -358,7 +364,7 @@ const Editor = forwardRef<HTMLDivElement>((_, ref) => {
358364
onClick={handleReset}
359365
className={styles.reset}
360366
>
361-
Reset Image
367+
{locales["option"]["reset"]}
362368
</TextButton>
363369
</li>
364370
</ul>

‎src/components/GuideDialog/GuideDialog.module.scss

+2
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929

3030
.guide {
3131
position: relative;
32+
text-align: center;
3233
width: min(45rem, 90%);
3334
padding: 2.5rem;
3435
font-weight: normal;
@@ -65,6 +66,7 @@
6566
overflow-y: auto;
6667
padding-right: 1.5rem;
6768
margin-right: -1.5rem;
69+
text-align: left;
6870

6971
@include mobile {
7072
padding-right: 1rem;

‎src/components/GuideDialog/GuideDialog.tsx

+6-13
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,9 @@
1313
* License for the specific language governing permissions and limitations
1414
* under the License.
1515
*/
16-
import { useEffect, useState } from "react";
17-
import readmeFile from "~/CONTRIBUTING.md";
18-
import Markdown from "react-markdown";
16+
import React, { Suspense } from "react";
1917

18+
import { Loading } from "@/components";
2019
import styles from "./GuideDialog.module.scss";
2120

2221
interface Props {
@@ -26,13 +25,7 @@ interface Props {
2625

2726
const GuideDialog = (props: Props) => {
2827
const { isVisible, onClose } = props;
29-
const [markdown, setMarkdown] = useState("");
30-
31-
useEffect(() => {
32-
fetch(readmeFile)
33-
.then((response) => response.text())
34-
.then(setMarkdown);
35-
}, []);
28+
const GuideMarkDown = React.lazy(() => import("./GuideMarkdown"));
3629

3730
if (!isVisible) {
3831
return <></>;
@@ -41,9 +34,9 @@ const GuideDialog = (props: Props) => {
4134
return (
4235
<div className={styles.dim} onClick={onClose}>
4336
<div className={styles.guide} onClick={(e) => e.stopPropagation()}>
44-
<div className={styles.markdown}>
45-
<Markdown disallowedElements={["img"]}>{`${markdown}`}</Markdown>
46-
</div>
37+
<Suspense fallback={<Loading />}>
38+
<GuideMarkDown />
39+
</Suspense>
4740
<button type="button" aria-label="Close guide modal" onClick={onClose}>
4841
<span className="material-symbols-outlined">cancel</span>
4942
</button>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/**
2+
* Copyright 2023 LINE Corporation
3+
*
4+
* LINE Corporation licenses this file to you under the Apache License,
5+
* version 2.0 (the "License"); you may not use this file except in compliance
6+
* with the License. You may obtain a copy of the License at:
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations
14+
* under the License.
15+
*/
16+
import { useEffect, useState } from "react";
17+
import config from "~/app.config.json";
18+
import Markdown from "react-markdown";
19+
20+
import { Config } from "@/constants/config";
21+
import styles from "./GuideDialog.module.scss";
22+
23+
const GuideMarkdown = () => {
24+
const { contributeGuide } = config as unknown as Config;
25+
const [markdown, setMarkdown] = useState("");
26+
27+
useEffect(() => {
28+
fetch(contributeGuide)
29+
.then((response) => response.text())
30+
.then(setMarkdown);
31+
}, [contributeGuide]);
32+
33+
return (
34+
<div className={styles.markdown}>
35+
<Markdown disallowedElements={["img"]}>{`${markdown}`}</Markdown>
36+
</div>
37+
);
38+
};
39+
40+
export default GuideMarkdown;

‎src/components/ImageButton/ImageButton.tsx

-1
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,6 @@ const ImageButton = (props: Props) => {
8080
<button type="button" onClick={onClick} className={styles.button}>
8181
<EditableImage
8282
src={src}
83-
selectedTheme={theme}
8483
fontColor={textColor}
8584
inputFields={inputFields}
8685
inputOptions={inputOptions}

‎src/components/ImageList/ImageList.module.scss

+26
Original file line numberDiff line numberDiff line change
@@ -66,5 +66,31 @@
6666
opacity: 0;
6767
}
6868
}
69+
70+
.dragging {
71+
z-index: 2;
72+
position: absolute;
73+
top: 0.25rem;
74+
left: 0.25rem;
75+
width: calc(100% - 0.5rem);
76+
height: calc(100% - 0.5rem);
77+
border: 1px dashed var(--red-primary);
78+
border-radius: 0.5rem;
79+
background-color: rgb(0 0 0 / 10%);
80+
pointer-events: none;
81+
animation: scale 0.3s;
82+
}
83+
}
84+
}
85+
86+
@keyframes scale {
87+
from {
88+
opacity: 0;
89+
transform: scale(0.8);
90+
}
91+
92+
to {
93+
opacity: 1;
94+
transform: scale(1);
6995
}
7096
}

‎src/components/ImageList/ImageList.tsx

+31-4
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,16 @@
1313
* License for the specific language governing permissions and limitations
1414
* under the License.
1515
*/
16-
import { MutableRefObject, useEffect, useRef } from "react";
16+
import { MutableRefObject, useEffect, useRef, useState } from "react";
1717

18-
import { ImageButton } from "@/components";
18+
import { Alert, AlertButton, ImageButton } from "@/components";
1919
import { Image } from "@/constants";
20-
import { useAppConfiguration, useDraggableScroll } from "@/hooks";
20+
import {
21+
useAppConfiguration,
22+
useDragAndDrop,
23+
useDraggableScroll,
24+
} from "@/hooks";
25+
import locales from "@/locales/en-US.json";
2126
import styles from "./ImageList.module.scss";
2227

2328
const ImageList = () => {
@@ -28,13 +33,25 @@ const ImageList = () => {
2833
handleChangeImage,
2934
customImages,
3035
handleChangeCustomImages,
36+
handleDropCustomImages,
3137
handleDeleteCustomImage,
3238
} = useAppConfiguration();
3339
const draggableElementRef = useRef<HTMLUListElement>(null);
3440
const { events } = useDraggableScroll(
3541
draggableElementRef as MutableRefObject<HTMLUListElement>,
3642
);
3743

44+
const dragRef = useRef<HTMLLabelElement>(null);
45+
const [showAlert, setShowAlert] = useState(false);
46+
const handleDrop = (e: DragEvent) => {
47+
try {
48+
handleDropCustomImages(e);
49+
} catch (e) {
50+
setShowAlert(true);
51+
}
52+
};
53+
const { isDragging } = useDragAndDrop(dragRef, handleDrop);
54+
3855
useEffect(() => {
3956
draggableElementRef?.current?.scrollTo(0, 0);
4057
}, [selectedTheme]);
@@ -73,7 +90,8 @@ const ImageList = () => {
7390
</li>
7491
))}
7592
<li>
76-
<label className={styles.file}>
93+
{isDragging && <span className={styles.dragging}></span>}
94+
<label className={styles.file} ref={dragRef}>
7795
<span className="material-symbols-outlined">add</span>
7896
<input
7997
type="file"
@@ -84,6 +102,15 @@ const ImageList = () => {
84102
</li>
85103
</>
86104
)}
105+
{showAlert && (
106+
<Alert onClose={() => setShowAlert(false)}>
107+
<strong>{locales["alert"]["uploadOnlyImages"]}</strong>
108+
<p>{locales["alert"]["makeSureImageExtensions"]}</p>
109+
<AlertButton onClick={() => setShowAlert(false)}>
110+
{locales["button"]["confirm"]}
111+
</AlertButton>
112+
</Alert>
113+
)}
87114
</ul>
88115
);
89116
};

‎src/components/TextInput/TextInput.tsx

+19
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
} from "react";
2424
import Draggable, { DraggableData, DraggableEvent } from "react-draggable";
2525

26+
import { Tooltip } from "@/components";
2627
import { Alignment, Font, FontSize, FontSizes } from "@/constants";
2728
import { useElementSize } from "@/hooks";
2829
import styles from "./TextInput.module.scss";
@@ -36,6 +37,7 @@ interface Props {
3637
style?: Font;
3738
alignment?: Alignment;
3839
label: string;
40+
tooltip?: string;
3941
defaultValue?: string;
4042
isFocused?: boolean;
4143
onFocus?: (event: FocusEvent<HTMLDivElement>) => void;
@@ -51,6 +53,7 @@ const TextInput = forwardRef<HTMLInputElement, Props>((props, ref) => {
5153
alignment = "left",
5254
label,
5355
defaultValue,
56+
tooltip,
5457
isFocused,
5558
onFocus,
5659
onBlur,
@@ -59,6 +62,7 @@ const TextInput = forwardRef<HTMLInputElement, Props>((props, ref) => {
5962
const dragRef = useRef<HTMLDivElement>(null);
6063
const [value, setValue] = useState("");
6164
const [dragPosition, setDragPosition] = useState(defaultPosition);
65+
const [isDragging, setIsDragging] = useState(false);
6266

6367
const placeholder = label.charAt(0).toUpperCase() + label.slice(1);
6468

@@ -67,8 +71,13 @@ const TextInput = forwardRef<HTMLInputElement, Props>((props, ref) => {
6771
setDragPosition(defaultPosition);
6872
};
6973

74+
const handleStart = () => {
75+
setIsDragging(true);
76+
};
77+
7078
const handleStop = (_: DraggableEvent, data: DraggableData) => {
7179
setDragPosition({ x: data.x, y: data.y });
80+
setIsDragging(false);
7281
};
7382

7483
const handleBlur = (event: FocusEvent<HTMLDivElement>) => {
@@ -105,6 +114,7 @@ const TextInput = forwardRef<HTMLInputElement, Props>((props, ref) => {
105114
disabled={window?.innerWidth < 768}
106115
defaultPosition={defaultPosition}
107116
position={dragPosition}
117+
onStart={handleStart}
108118
onStop={handleStop}
109119
>
110120
<div
@@ -126,6 +136,9 @@ const TextInput = forwardRef<HTMLInputElement, Props>((props, ref) => {
126136
style={{
127137
fontFamily: `'${style}', 'LINE Seed', 'LINE Seed KR', serif`,
128138
}}
139+
autoComplete={"new-password"}
140+
aria-autocomplete={"none"}
141+
list={"autocomplete-off"}
129142
/>
130143
</label>
131144
<span className={styles.tag}>{label}</span>
@@ -144,6 +157,12 @@ const TextInput = forwardRef<HTMLInputElement, Props>((props, ref) => {
144157
>
145158
{value?.length === 0 ? placeholder : value}
146159
</span>
160+
<Tooltip
161+
visible={Boolean(isFocused && tooltip) && !isDragging}
162+
style={{ transform: `${dragRef?.current?.style.transform}` }}
163+
>
164+
{tooltip}
165+
</Tooltip>
147166
</div>
148167
);
149168
});
+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
.tooltip {
2+
@include font("16");
3+
4+
z-index: 9;
5+
position: absolute;
6+
top: -4.5rem;
7+
left: 0;
8+
display: inline-flex;
9+
align-items: center;
10+
justify-content: center;
11+
padding: 0.5rem 0.75rem 0.5rem 0.5rem;
12+
border-radius: 0.5rem;
13+
background-color: #3f0;
14+
color: #000;
15+
white-space: nowrap;
16+
17+
:global {
18+
.material-symbols-outlined {
19+
font-size: 1.25rem;
20+
margin-right: 0.25rem;
21+
}
22+
}
23+
24+
&::before {
25+
content: "";
26+
height: 0;
27+
width: 0;
28+
position: absolute;
29+
bottom: 0;
30+
left: 0.5rem;
31+
transform: translate(0, 100%);
32+
border-right: solid 0.3rem transparent;
33+
border-left: solid 0.3rem transparent;
34+
border-top: solid 0.3rem #3f0;
35+
}
36+
}

‎src/components/Tooltip/Tooltip.tsx

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { HTMLAttributes, PropsWithChildren } from "react";
2+
3+
import styles from "./Tooltip.module.scss";
4+
5+
interface Props extends PropsWithChildren, HTMLAttributes<HTMLElement> {
6+
visible: boolean;
7+
}
8+
9+
const Tooltip = (props: Props) => {
10+
const { children, visible, style } = props;
11+
if (!visible) {
12+
return <></>;
13+
}
14+
return (
15+
<strong className={styles.tooltip} style={style}>
16+
<span className="material-symbols-outlined">lightbulb</span>
17+
{children}
18+
</strong>
19+
);
20+
};
21+
22+
export default Tooltip;

‎src/components/Tooltip/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default as Tooltip } from "./Tooltip";

‎src/components/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,5 @@ export { TextInput } from "./TextInput";
3030
export { EditableImage } from "./EditableImage";
3131
export { DragAndDropFile } from "./DragAndDropFile";
3232
export { SyncButton } from "./SyncButton";
33+
export { Alert, AlertButton } from "./Alert";
34+
export { Tooltip } from "./Tooltip";

‎src/constants/config.ts

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/**
2+
* Copyright 2023 LINE Corporation
3+
*
4+
* LINE Corporation licenses this file to you under the Apache License,
5+
* version 2.0 (the "License"); you may not use this file except in compliance
6+
* with the License. You may obtain a copy of the License at:
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations
14+
* under the License.
15+
*/
16+
import { InputFieldGroup } from "@/constants/input";
17+
import { Theme } from "@/constants/theme";
18+
19+
export interface Config {
20+
title: {
21+
text: string;
22+
logo: string;
23+
};
24+
keyColor: {
25+
light: string;
26+
dark: string;
27+
};
28+
backgroundsUri: {
29+
type: string;
30+
path: string;
31+
};
32+
contributeGuide: string;
33+
fonts: {
34+
sizes: Record<string, string>;
35+
styles: Array<string>;
36+
};
37+
defaultInputFields: Array<InputFieldGroup>;
38+
themes: Array<Theme>;
39+
}

‎src/constants/font.ts

+7-5
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,18 @@
1515
*/
1616
import config from "~/output.config.json";
1717

18+
import { Config } from "@/constants/config";
19+
20+
const { fonts } = config as unknown as Config;
21+
1822
export const FontSizes: Record<string, string> = {} as const;
19-
Object.keys(config.fonts.sizes).forEach(
20-
(size) =>
21-
(FontSizes[size] =
22-
config.fonts.sizes[size as keyof typeof config.fonts.sizes]),
23+
Object.keys(fonts.sizes).forEach(
24+
(size) => (FontSizes[size] = fonts.sizes[size as keyof typeof fonts.sizes]),
2325
);
2426
export type FontSize = keyof typeof FontSizes;
2527

2628
export const Fonts: Record<string, string> = {} as const;
27-
config.fonts.styles.forEach((font) => (Fonts[font] = font));
29+
fonts.styles.forEach((font) => (Fonts[font] = font));
2830
export type Font = keyof typeof Fonts;
2931

3032
export const Alignments = {

‎src/constants/input.ts

+1
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,5 @@ export interface InputField {
2424
isRequired?: boolean;
2525
isVisible?: boolean;
2626
text?: string;
27+
tooltip?: string;
2728
}

‎src/constants/theme.ts

+1
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ export interface Theme {
55
backgrounds: Array<Image>;
66
inputFields?: Array<InputFieldGroup>;
77
isNew?: boolean;
8+
isHidden?: boolean;
89
}

‎src/hooks/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
* under the License.
1515
*/
1616
export { default as useDraggableScroll } from "./useDraggableScroll";
17+
export { default as useDragAndDrop } from "./useDragAndDrop";
1718
export { default as useElementSize } from "./useElementSize";
1819
export { default as useEventListener } from "./useEventListener";
1920
export { default as useImageColor } from "./useImageColor";

‎src/hooks/useAppConfiguration/AppContext.tsx

+47-33
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import config from "~/output.config.json";
2626
import axios, { AxiosError } from "axios";
2727

2828
import { Image, InputFieldGroup, Theme } from "@/constants";
29+
import { Config } from "@/constants/config";
2930
import { readConfigurationFromGithub } from "@/utils";
3031

3132
// Create a custom axios instance with default headers
@@ -69,8 +70,11 @@ export const AppContext = createContext<ContextProps>({
6970

7071
export const AppProvider = (props: PropsWithChildren) => {
7172
const { children } = props;
72-
const { backgroundsUri } = config;
73-
const { defaultInputFields } = config;
73+
const {
74+
backgroundsUri,
75+
defaultInputFields,
76+
themes: configThemes,
77+
} = config as unknown as Config;
7478
const [themes, setThemes] = useState<Array<Theme>>([]);
7579
const [selectedImage, setSelectedImage] = useState<Image>({
7680
src: "",
@@ -79,39 +83,49 @@ export const AppProvider = (props: PropsWithChildren) => {
7983
const [customImages, setCustomImages] = useState<Array<Image>>([]);
8084
const [isSyncing, setIsSyncing] = useState(false);
8185

82-
const resetThemes = async (path: string, type: string) => {
83-
const lowerCasedType = type.trim().toLowerCase();
84-
switch (lowerCasedType) {
85-
case "filesystem":
86-
case "cdn":
87-
setThemes(config.themes as Array<Theme>);
88-
setSelectedImage((config.themes[0] as Theme)?.backgrounds[0] ?? []);
89-
break;
90-
case "github":
91-
try {
92-
const themes = await readConfigurationFromGithub(githubAxios, path);
93-
if (themes) {
94-
setThemes(themes);
95-
setSelectedImage(themes[0].backgrounds[0]);
96-
}
97-
} catch (error) {
98-
console.error(
99-
`Error reading configuration from github:`,
100-
(error as AxiosError).message,
86+
const resetThemes = useCallback(
87+
async (path: string, type: string) => {
88+
const lowerCasedType = type.trim().toLowerCase();
89+
switch (lowerCasedType) {
90+
case "filesystem":
91+
case "cdn":
92+
setThemes(
93+
configThemes.filter(
94+
({ isHidden = false }) => !isHidden,
95+
) as Array<Theme>,
10196
);
102-
}
103-
break;
104-
default:
105-
console.warn("Unknown type:", type);
106-
}
107-
};
97+
setSelectedImage((configThemes[0] as Theme)?.backgrounds[0] ?? []);
98+
break;
99+
case "github":
100+
try {
101+
const themes = await readConfigurationFromGithub(githubAxios, path);
102+
if (themes) {
103+
setThemes(themes.filter(({ isHidden = false }) => !isHidden));
104+
setSelectedImage(themes[0].backgrounds[0]);
105+
}
106+
} catch (error) {
107+
console.error(
108+
`Error reading configuration from github:`,
109+
(error as AxiosError).message,
110+
);
111+
}
112+
break;
113+
default:
114+
console.warn("Unknown type:", type);
115+
}
116+
},
117+
[configThemes],
118+
);
108119

109120
const handleDropCustomImages = (event: DragEvent) => {
110-
const src = URL.createObjectURL(
111-
((event.dataTransfer as DataTransfer).files as FileList)[0],
112-
);
113-
setCustomImages([...customImages, { src, theme: "custom" }]);
121+
const file = ((event.dataTransfer as DataTransfer).files as FileList)[0];
122+
const src = URL.createObjectURL(file);
114123

124+
if (file.type.includes("image")) {
125+
setCustomImages([...customImages, { src, theme: "custom" }]);
126+
} else {
127+
throw new Error("FileNotAcceptable");
128+
}
115129
return () => URL.revokeObjectURL(src);
116130
};
117131

@@ -144,11 +158,11 @@ export const AppProvider = (props: PropsWithChildren) => {
144158
}, 2000);
145159

146160
return () => clearTimeout(timer);
147-
}, [backgroundsUri]);
161+
}, [backgroundsUri, resetThemes]);
148162

149163
useEffect(() => {
150164
backgroundsUri && resetThemes(backgroundsUri.path, backgroundsUri.type);
151-
}, [backgroundsUri]);
165+
}, [backgroundsUri, resetThemes]);
152166

153167
useEffect(() => {
154168
if (selectedTheme === "custom") {

‎src/hooks/useDragAndDrop.ts

+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/**
2+
* Copyright 2023 LINE Corporation
3+
*
4+
* LINE Corporation licenses this file to you under the Apache License,
5+
* version 2.0 (the "License"); you may not use this file except in compliance
6+
* with the License. You may obtain a copy of the License at:
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations
14+
* under the License.
15+
*/
16+
import { MutableRefObject, useCallback, useEffect, useState } from "react";
17+
18+
function useDragAndDrop(
19+
ref: MutableRefObject<HTMLElement | null>,
20+
onDrop: (e: DragEvent) => void,
21+
) {
22+
const [isDragging, setIsDragging] = useState(false);
23+
24+
const handleDragIn = useCallback((e: DragEvent): void => {
25+
e.preventDefault();
26+
e.stopPropagation();
27+
}, []);
28+
29+
const handleDragOut = useCallback((e: DragEvent): void => {
30+
e.preventDefault();
31+
e.stopPropagation();
32+
33+
setIsDragging(false);
34+
}, []);
35+
36+
const handleDragOver = useCallback((e: DragEvent): void => {
37+
e.preventDefault();
38+
e.stopPropagation();
39+
40+
if (e.dataTransfer!.files) {
41+
setIsDragging(true);
42+
}
43+
}, []);
44+
45+
const handleDrop = useCallback(
46+
(e: DragEvent) => {
47+
e.preventDefault();
48+
e.stopPropagation();
49+
onDrop(e);
50+
setIsDragging(false);
51+
},
52+
[onDrop],
53+
);
54+
55+
const initDragEvents = useCallback(() => {
56+
if (ref.current !== null) {
57+
ref.current.addEventListener("dragenter", handleDragIn);
58+
ref.current.addEventListener("dragleave", handleDragOut);
59+
ref.current.addEventListener("dragover", handleDragOver);
60+
ref.current.addEventListener("drop", handleDrop);
61+
}
62+
}, [handleDragIn, handleDragOut, handleDragOver, handleDrop, ref]);
63+
64+
const resetDragEvents = useCallback(() => {
65+
if (ref.current !== null) {
66+
ref.current.removeEventListener("dragenter", handleDragIn);
67+
ref.current.removeEventListener("dragleave", handleDragOut);
68+
ref.current.removeEventListener("dragover", handleDragOver);
69+
ref.current.removeEventListener("drop", handleDrop);
70+
}
71+
}, [handleDragIn, handleDragOut, handleDragOver, handleDrop, ref]);
72+
73+
useEffect(() => {
74+
initDragEvents();
75+
76+
return () => resetDragEvents();
77+
}, [initDragEvents, resetDragEvents]);
78+
79+
return {
80+
isDragging,
81+
};
82+
}
83+
84+
export default useDragAndDrop;

‎src/hooks/useSnapshot.ts

+21-4
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,35 @@
1313
* License for the specific language governing permissions and limitations
1414
* under the License.
1515
*/
16-
import { useCallback, useState } from "react";
16+
import React, { useCallback, useState } from "react";
1717
import html2Canvas from "html2canvas";
1818

19+
const size = {
20+
width: 1152,
21+
height: 648,
22+
};
23+
1924
const useSnapshot = (dom: React.RefObject<HTMLElement>) => {
2025
const [loading, setLoading] = useState<boolean>(false);
2126

2227
const saveImage = useCallback(
2328
async (fileName: string = "image.png") => {
2429
if (!loading && dom.current) {
2530
setLoading(true);
26-
27-
html2Canvas(dom.current)
31+
html2Canvas(dom.current, {
32+
width: size.width,
33+
height: size.height,
34+
scale: 1,
35+
onclone: (_, element) => {
36+
element
37+
.getElementsByTagName("img")[0]
38+
.setAttribute("style", "border-radius: 0; box-shadow: none;");
39+
element.setAttribute(
40+
"style",
41+
`--text-scale: 1; width: ${size.width}px; height: ${size.height}px`,
42+
);
43+
},
44+
})
2845
.then((blob) => {
2946
const base64Image = blob.toDataURL("image/png");
3047
const link = document.createElement("a");
@@ -35,7 +52,7 @@ const useSnapshot = (dom: React.RefObject<HTMLElement>) => {
3552
setLoading(false);
3653
})
3754
.catch((err) => {
38-
console.log(err);
55+
console.error(err);
3956
setLoading(false);
4057
});
4158
}

‎src/locales/en-US.json

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"title": {
3+
"theme": "Select Theme"
4+
},
5+
"option": {
6+
"text": "Text Option",
7+
"image": "Image Option",
8+
"flip": "Horizontal Flip",
9+
"reset": "Reset Style",
10+
"font": "Font",
11+
"size": "Size",
12+
"color": "Color",
13+
"opacity": "Opacity",
14+
"alignment": "Alignment"
15+
},
16+
"button": {
17+
"download": "Download",
18+
"contribute": "How To Contribute",
19+
"github": "Github",
20+
"confirm": "Confirm"
21+
},
22+
"alert": {
23+
"uploadOnlyImages": "Only images can be uploaded.",
24+
"makeSureImageExtensions": "Please make sure that the file you want to upload\nhas the following extensions: .jpg, .jpeg, .png, .gif, .bmp and .webp."
25+
}
26+
}

‎tsconfig.json

+1-2
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,5 @@
2828

2929
"types": ["vite-plugin-svgr/client", "node"]
3030
},
31-
"include": ["src"],
32-
"references": [{ "path": "./tsconfig.node.json" }]
31+
"include": ["src", "vite.config.ts", "app.config.json"]
3332
}

‎tsconfig.node.json

-10
This file was deleted.

‎vite.config.ts

+49-18
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import stylelint from "vite-plugin-stylelint";
77
import svgr from "vite-plugin-svgr";
88
import tsconfigPaths from "vite-tsconfig-paths";
99

10+
import type { Config } from "@/constants/config.ts";
11+
import type { Image } from "@/constants/image.ts";
1012
import config from "./app.config.json";
1113

1214
// https://vitejs.dev/config/
@@ -19,6 +21,7 @@ export default defineConfig({
1921
stylelint({
2022
fix: true,
2123
}),
24+
copyContributeGuide(),
2225
copyBackgroundsForFileSystem(),
2326
],
2427
css: {
@@ -31,36 +34,64 @@ export default defineConfig({
3134
assetsInclude: ["**/*.md"],
3235
});
3336

37+
function copyContributeGuide() {
38+
const { contributeGuide } = config as unknown as Config;
39+
const targetDirectory = "dist";
40+
41+
return copy({
42+
targets: [
43+
{
44+
src: contributeGuide,
45+
dest: targetDirectory,
46+
},
47+
],
48+
hook: "writeBundle",
49+
});
50+
}
51+
3452
/**
3553
* If config has `filesystem` type for `backgroundsUri`, copy local backgrounds path to dist folder
3654
*/
3755
function copyBackgroundsForFileSystem() {
56+
const { backgroundsUri, themes } = config as unknown as Config;
57+
3858
if (
39-
config.backgroundsUri &&
40-
config.backgroundsUri.type === "filesystem" &&
41-
config.backgroundsUri.path
59+
backgroundsUri &&
60+
backgroundsUri.type === "filesystem" &&
61+
backgroundsUri.path
4262
) {
4363
const dir = path.resolve();
44-
const sourceDirectory = config.backgroundsUri.path;
64+
const sourceDirectory = backgroundsUri.path;
4565
const targetDirectory = "dist";
46-
const order = config.themes.map(({ name }) => name);
66+
const order = themes.map(({ name }) => name);
67+
const filteredOrder = themes
68+
.filter(({ isHidden }) => !isHidden)
69+
.map(({ name }) => name);
4770

4871
const fileContentsArray = readFilesRecursively(sourceDirectory);
4972

5073
order.map((theme, index) => {
74+
const isThemeHidden =
75+
themes.filter(({ name }) => name === theme)[0]?.isHidden ?? false;
5176
const filteredFileContentsArray = fileContentsArray.filter(
5277
({ theme: fileTheme }) => fileTheme === theme,
5378
);
54-
config.themes[index]["backgrounds"] = filteredFileContentsArray.map(
55-
({ src, fontColor }) => (fontColor ? { src, fontColor } : { src }),
56-
);
79+
80+
themes[index]["backgrounds"] = isThemeHidden
81+
? []
82+
: filteredFileContentsArray.map(({ src, fontColor }) =>
83+
fontColor ? { src, fontColor } : { src },
84+
);
5785
});
5886

5987
const configJsonPath = path.join(dir, "output.config.json");
6088
fs.writeFileSync(configJsonPath, JSON.stringify(config, null, 2));
6189

6290
return copy({
63-
targets: [{ src: sourceDirectory, dest: targetDirectory }],
91+
targets: filteredOrder.map((theme) => ({
92+
src: path.join(sourceDirectory, theme),
93+
dest: path.join(targetDirectory, backgroundsUri.path),
94+
})),
6495
hook: "writeBundle",
6596
});
6697
} else {
@@ -70,22 +101,22 @@ function copyBackgroundsForFileSystem() {
70101
}
71102
}
72103

73-
function readFilesRecursively(dir) {
74-
const fileContentsArray = [];
104+
function readFilesRecursively(dir: string) {
105+
const { backgroundsUri, themes } = config as unknown as Config;
106+
const fileContentsArray: Image[] = [];
75107

76-
const readFiles = (dir) => {
108+
const readFiles = (dir: string) => {
77109
const files = fs.readdirSync(dir);
78-
const order = config.themes.map(({ name }) => name);
110+
const order = themes
111+
.filter(({ isHidden = false }) => !isHidden)
112+
.map(({ name }) => name);
79113
const sortedFiles = files.sort(
80114
(a, b) => order.indexOf(a) - order.indexOf(b),
81115
);
82116

83117
sortedFiles.forEach((file) => {
84118
const filePath = path.join(dir, file);
85-
const theme = dir.replace(
86-
path.join(config.backgroundsUri.path, path.sep),
87-
"",
88-
);
119+
const theme = dir.replace(path.join(backgroundsUri.path, path.sep), "");
89120

90121
if (fs.statSync(filePath).isDirectory()) {
91122
readFiles(filePath);
@@ -110,7 +141,7 @@ function readFilesRecursively(dir) {
110141
return fileContentsArray;
111142
}
112143

113-
function isImageFile(filePath) {
144+
function isImageFile(filePath: string) {
114145
const allowedExtensions = [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp"];
115146
const extension = path.extname(filePath).toLowerCase();
116147
return allowedExtensions.includes(extension);

0 commit comments

Comments
 (0)
Please sign in to comment.