Minigame Editor Demo 16/10/2024

This commit is contained in:
2024-10-16 10:20:17 +07:00
parent 98ec6fe8ce
commit 89489d62a9
64 changed files with 8918 additions and 0 deletions

24
minigame-editor/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -0,0 +1,8 @@
# React + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh

View File

@@ -0,0 +1,38 @@
import js from '@eslint/js'
import globals from 'globals'
import react from 'eslint-plugin-react'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
export default [
{ ignores: ['dist'] },
{
files: ['**/*.{js,jsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
ecmaVersion: 'latest',
ecmaFeatures: { jsx: true },
sourceType: 'module',
},
},
settings: { react: { version: '18.3' } },
plugins: {
react,
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...js.configs.recommended.rules,
...react.configs.recommended.rules,
...react.configs['jsx-runtime'].rules,
...reactHooks.configs.recommended.rules,
'react/jsx-no-target-blank': 'off',
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
]

View File

@@ -0,0 +1,21 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Chakra+Petch:wght@300;400;500;600;700&display=swap">
<title>Vite + React</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

5952
minigame-editor/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,36 @@
{
"name": "vite-project",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@reduxjs/toolkit": "^2.2.8",
"@toast-ui/react-image-editor": "^3.15.2",
"react": "^18.3.1",
"react-color": "^2.19.3",
"react-contenteditable": "^3.3.7",
"react-dom": "^18.3.1",
"react-images-uploading": "^3.1.7",
"react-redux": "^9.1.2",
"react-tooltip": "^5.28.0",
"styled-components": "^6.1.13"
},
"devDependencies": {
"@eslint/js": "^9.11.1",
"@types/react": "^18.3.10",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react-swc": "^3.5.0",
"eslint": "^9.11.1",
"eslint-plugin-react": "^7.37.0",
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
"eslint-plugin-react-refresh": "^0.4.12",
"globals": "^15.9.0",
"vite": "^5.4.8"
}
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

369
minigame-editor/src/App.css Normal file
View File

@@ -0,0 +1,369 @@
.container {
max-width: 410px;
width: 100%;
padding: 0 10px;
margin: 0 auto;
}
.title {
text-align: center;
}
.header {
text-align: center;
}
.main .circle {
position: relative;
margin: 48px 0 12px;
text-align: center;
}
.main .circle-bg {
width: 100%;
max-width: 100%;
}
.main .circle-item {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.main .circle-item.rw {
max-width: 82%;
top: 49.2%;
}
.main .circle-item.ar {
top: -25px;
transform: translate(-50%, 0);
max-width: 51px;
}
.main .circle-item.btn {
max-width: 132px;
}
.main .noffy {
text-align: center;
}
.main .noffy-number {
color: #ffb71d;
}
.main .noffy-remain {
display: inline-block;
background: url(src/assets/images/hnc-game-2-btn-1.png) no-repeat;
background-size: contain;
width: 223px;
height: 38px;
font-size: 20px;
font-weight: 600;
line-height: 34px;
text-align: center;
color: #fff;
margin: 0 0 12px;
}
.main .noffy-remain .noffy-number {
font-size: 24px;
}
.main .noffy-player {
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
max-width: 411px;
font-size: 15px;
line-height: 34px;
padding: 0 15px 0 30px;
margin: 0 auto;
background: #4613d3;
color: #fff;
border: 1px dashed #fff;
border-radius: 20px;
}
.main .box {
background-size: 100% 100%;
background-repeat: no-repeat;
padding: 38px 27px 22px 34px;
margin: 16px 0;
}
.main .result {
background-image: url(src/assets/images/hnc-game-2-box-1.png);
min-height: 287px;
}
.main .result-heading {
background: linear-gradient(90deg, #ff74ec 26%, #a43dff 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.main .result-list {
list-style: none;
}
.main .result-item {
display: flex;
align-items: center;
gap: 12px;
min-height: 57px;
font-size: 14px;
line-height: 21px;
margin-bottom: 6px;
padding: 0 10px;
color: #000;
background: #fff;
clip-path: polygon(50% 0%, 100% 0, 100% 85%, 97% 100%, 0 100%, 0 15%, 3% 0);
border-radius: 13px 3px 13px 3px;
}
.main .result-no {
max-width: max-content;
font-weight: 700;
line-height: 39px;
text-transform: uppercase;
text-align: center;
padding: 0 5px;
color: #fff;
background: linear-gradient(90deg, #ff74ec 26%, #a43dff 100%);
clip-path: polygon(50% 0%, 100% 0, 100% 80%, 85% 100%, 0 100%, 0 20%, 15% 0);
border-radius: 10px 3px 10px 3px;
}
.main .reward {
background-image: url(src/assets/images/hnc-game-2-box-2.png);
min-height: 633px;
}
.main .reward-list {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 11px;
}
.main .reward-item {
font-size: 12px;
line-height: 14px;
text-align: center;
color: #002a88;
background: #fff;
clip-path: polygon(50% 0%, 100% 0, 100% 94%, 94% 100%, 0 100%, 0 6%, 6% 0);
border-radius: 13px 3px 13px 3px;
}
.main .reward-top {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
justify-content: center;
text-transform: uppercase;
font-weight: 500;
padding: 8px 8px 6px;
}
.main .reward-img {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 111px;
height: 111px;
}
.main .reward-img::after {
content: "";
display: none;
position: absolute;
top: 0;
left: -25px;
width: 60px;
height: 60px;
background-size: contain;
background-repeat: no-repeat;
}
.main .reward-no {
font-size: 20px;
line-height: 16px;
font-weight: 700;
}
.main .reward-name {
padding: 6px 0 0;
}
.main .reward-price {
font-size: 13px;
padding: 9px 8px 8px;
border-top: 1px dashed #909090;
text-align: center;
}
.main .reward-bold {
font-weight: 700;
color: #ff0000;
}
.main .reward-item:first-child {
grid-column: span 2;
font-size: 15px;
line-height: 16px;
text-align: left;
background: #ffd737;
clip-path: polygon(50% 0%, 100% 0, 100% 91%, 96% 100%, 0 100%, 0 9%, 4% 0);
border-radius: 17px 3px 17px 3px;
}
.main .reward-item:first-child .reward-top {
flex-direction: row;
gap: 12px;
padding: 8px 8px 6px 26px;
}
.main .reward-item:first-child .reward-no {
font-size: 24px;
}
.main .reward-item:first-child .reward-img::after {
display: block;
background-image: url(src/assets/images/hnc-game-2-icon-top-1.png);
}
.main .reward-item:nth-child(2) .reward-img::after {
display: block;
background-image: url(src/assets/images/hnc-game-2-icon-top-2.png);
}
.main .reward-item:nth-child(3) .reward-img::after {
display: block;
background-image: url(src/assets/images/hnc-game-2-icon-top-3.png);
}
.main .policy {
padding-top: 16px;
background-image: url(src/assets/images/hnc-game-2-box-3.png);
min-height: 394px;
}
.main .popup-heading {
background: linear-gradient(90deg, #ff74ec 26%, #a43dff 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.main .hotline {
text-align: center;
padding: 8px 0 10px;
}
.main .hotline .tel {
display: inline-flex;
align-items: center;
gap: 7px;
font-size: 14px;
line-height: 21px;
text-transform: uppercase;
color: #fff;
}
.main .hotline .tel b {
font-size: 18px;
line-height: 27px;
}
.main .popup {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 9;
}
.main .popup-bg {
background: rgba(0, 0, 0, 0.7);
width: 100%;
height: 100%;
}
.main .popup-main {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
max-width: 408px;
width: 100%;
}
.main .popup-close {
position: absolute;
top: -11px;
right: -11px;
width: 44px;
height: 49px;
background-image: url(src/assets/images/hnc-game-2-btn-close-1.png);
background-repeat: no-repeat;
background-size: contain;
}
.main .popup-box {
background-image: url(src/assets/images/hnc-game-2-box-4.png);
background-repeat: no-repeat;
background-size: 100% 100%;
min-height: 298px;
padding: 21px 27px 18px 22px;
}
.main .popup-box-2 {
padding-top: 0;
}
.main .popup-result {
position: relative;
}
.main .popup-reward {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.main .popup-form {
text-align: center;
}
.main .form-input {
width: 100%;
font-size: 14px;
line-height: 14px;
border: 1px solid #dcdcdc;
border-radius: 6px;
padding: 6px 11px;
margin-bottom: 10px;
}
.main .form-btn {
width: 175px;
height: 49px;
font-size: 16px;
line-height: 24px;
font-weight: 700;
text-align: center;
text-transform: uppercase;
margin-top: 10px;
color: #fff;
background-image: url(src/assets/images/hnc-game-2-btn-2.png);
background-repeat: no-repeat;
background-size: contain;
}

View File

@@ -0,0 +1,29 @@
import { useState } from 'react'
import { useSelector } from 'react-redux';
import styled from 'styled-components'
import './App.css'
import Template from './Template'
import Editor from './Editor'
const Container = styled.div`
display: flex;
`
function App() {
const [target, setTarget] = useState("main");
const database = useSelector((state) => state.database.value);
const props = database[target];
// console.log(database);
return (
<Container>
<Template setTarget={setTarget} props={database} />
<Editor props={props} />
</Container>
)
}
export default App

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -0,0 +1,19 @@
import styled from 'styled-components'
const StyledBox = styled.div`
${props => props.$sty}
`
function Box({ props }) {
const cla = props.className;
const id = props.id;
const style = props.styles;
return (
<>
<StyledBox id={id} className={"ld-item " + cla} $sty={style} />
</>
)
}
export default Box

View File

@@ -0,0 +1,51 @@
import { useRef } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { updateDatabase } from '../database/database'
import styled from 'styled-components'
import ContentEditable from 'react-contenteditable'
const StyledContentEditable = styled(ContentEditable)`
${props => props.$sty}
`
function Editable({ props }) {
const database = useSelector((state) => state.database.value);
const dispatch = useDispatch();
const content = useRef(props.content);
const handleChange = evt => {
content.current = evt.target.value;
};
const handleBlur = () => {
const newParams = { ...database[id], content: content.current };
const newDatabase = { ...database, [id]: newParams };
dispatch(updateDatabase(newDatabase));
}
const handlePaste = (evt) => {
evt.preventDefault();
const copyText = (evt.originalEvent || evt).clipboardData.getData('text/plain');
document.execCommand("insertHTML", false, copyText);
}
const style = props.styles;
return (
<>
<StyledContentEditable
html={content.current}
onBlur={handleBlur}
onChange={handleChange}
onPaste={handlePaste}
tagName={props.tagName}
className={'ld-item ' + props.className}
id={props.id}
$sty={style}
/>
</>
)
}
export default Editable

View File

@@ -0,0 +1,20 @@
import styled from 'styled-components'
const StyledImage = styled.img`
${props => props.$sty}
`
function Image({ props }) {
const attr = props.attributes;
const cla = props.className;
const id = props.id;
const style = props.styles;
return (
<>
<StyledImage id={id} className={"ld-item " + cla} src={attr.src} alt={attr.alt} width={attr.width} height={attr.height} $sty={style} />
</>
)
}
export default Image

View File

@@ -0,0 +1,194 @@
{
"main": {
"id": "main",
"type": "main",
"tagName": "div",
"content": "",
"className": "main",
"attributes": {},
"styles": {
"background": "url(src/assets/images/hnc-game-2-bg-1.png)",
"backgroundRepeat": "no-repeat",
"backgroundSize": "cover"
}
},
"logo": {
"id": "logo",
"type": "image",
"tagName": "img",
"content": "",
"className": "logo",
"attributes": {
"src": "src/assets/images/hnc-game-2-logo-1.png",
"alt": "Hacom",
"width": "100",
"height": "38"
},
"styles": {
"display": "",
"width": "100px",
"height": "auto",
"padding": "8px 0 12px",
"margin": "0"
}
},
"banner": {
"id": "banner",
"type": "image",
"tagName": "img",
"content": "",
"className": "banner",
"attributes": {
"src": "src/assets/images/hnc-game-2-title-1.png",
"alt": "Vòng quay may mắn",
"width": "289",
"height": "106"
},
"styles": {
"width": "289px",
"height": "auto",
"padding": "0",
"margin": "0"
}
},
"date": {
"id": "date",
"type": "text",
"tagName": "div",
"content": "01/09/2024 - 17/09/2024",
"className": "date",
"attributes": {},
"styles": {
"text-align": "center",
"font-size": "18px",
"font-weight": "600",
"line-height": "27px",
"color": "#fff"
}
},
"circle_background": {
"id": "circle_background",
"type": "image",
"tagName": "img",
"content": "",
"className": "circle-bg",
"attributes": {
"src": "src/assets/images/hnc-game-2-cricle-1.png"
},
"styles": {}
},
"circle_arrow": {
"id": "circle_arrow",
"type": "image",
"tagName": "img",
"content": "",
"className": "circle-item ar",
"attributes": {
"src": "src/assets/images/hnc-game-2-cricle-3.png"
},
"styles": {}
},
"circle_button": {
"id": "circle_button",
"type": "image",
"tagName": "img",
"content": "",
"className": "circle-bg",
"attributes": {
"src": "src/assets/images/hnc-game-2-cricle-4.png"
},
"styles": {}
},
"reward": {
"id": "reward",
"type": "reward",
"tagName": "div",
"content": "",
"className": "",
"attributes": {
"limit": "5"
},
"styles": {
"background": "url(src/assets/images/hnc-game-2-box-2.png)",
"background-size": "100% 100%",
"background-repeat": "no-repeat",
"padding": "38px 27px 22px 34px",
"margin": "16px 0",
"min-height": "633px"
}
},
"reward_heading": {
"id": "reward_heading",
"type": "text",
"tagName": "h2",
"content": "Danh sách giải thưởng",
"className": "heading reward-heading",
"attributes": {},
"styles": {
"font-size": "20px",
"line-height": "30px",
"text-align": "center",
"text-transform": "uppercase",
"margin-bottom": "14px",
"color": "#73dff8"
}
},
"policy_heading": {
"id": "policy_heading",
"type": "text",
"tagName": "h2",
"content": "Thể lệ chương trình",
"className": "heading policy-heading",
"attributes": {},
"styles": {
"font-size": "20px",
"line-height": "30px",
"text-align": "center",
"text-transform": "uppercase",
"margin-bottom": "14px",
"background": "linear-gradient(90deg, #ff74ec 26%, #a43dff 100%)",
"-webkit-background-clip": "text",
"-webkit-text-fill-color": "transparent"
}
},
"policy_content": {
"id": "policy_content",
"type": "text",
"tagName": "div",
"content": "1. Quý khách cần đăng nhập Thông tin chính xác để quay và nhận thưởng<br><br>2. Phiếu mua hàng chỉ có giá trị tại Hacom trong thời gian chương trình diễn ra và không thể chuyển nhượng<br><br>3. Phiếu mua hàng không quy đổi thành tiền mặt, không hoàn lại, chỉ sử dụng 01 lần<br><br>4. Chương trình không áp dụng đồng thời với ưu đãi XXX<br><br>5. Mọi khiếu nại liên quan đến chương tình sẽ được giải quyết theo quyết định của Hacom. Quyết định của Hacom là quyết định cuối cùng",
"className": "policy-content",
"attributes": {},
"styles": {
"font-size": "14px",
"line-height": "20px",
"text-align": "left",
"color": "#000"
}
},
"policy_hotline": {
"id": "policy_hotline",
"type": "text",
"tagName": "b",
"content": "1900 1903",
"className": "",
"attributes": {},
"styles": {}
},
"footer": {
"id": "footer",
"type": "text",
"tagName": "div",
"content": "© 2024 Công ty Cổ phần đầu tư công nghệ HACOM<br>Trụ sở chính: Số 129+131 Lê Thanh Nghị, Phường Đồng Tâm, Quận Hai Bà Trưng, Thành phố Hà Nội<br>VPGD: Tầng 3 Tòa nhà LILAMA, số 124 Minh Khai, Phường Minh Khai, Quận Hai Bà Trưng, Thành phố Hà Nội<br>GPĐKKD số 0101161194 do Sở KHĐT Tp.Hà Nội cấp ngày 31/8/2001<br>Email: info@hacom.vn. Điện thoại: 1900 1903",
"className": "footer",
"attributes": {},
"styles": {
"font-size": "14px",
"font-weight": "300",
"line-height": "20px",
"text-align": "center",
"padding": "13px",
"color": "#fff",
"background": "#2d3075"
}
}
}

View File

@@ -0,0 +1,19 @@
import { createSlice } from '@reduxjs/toolkit';
import initialState from './database.json';
export const Database = createSlice({
name: 'database',
initialState: {
value: initialState
},
reducers: {
updateDatabase: (state, newState) => {
state.value = newState.payload;
},
},
})
// Action creators are generated for each case reducer function
export const { updateDatabase } = Database.actions
export default Database.reducer

View File

@@ -0,0 +1,12 @@
{
"template-1": {
"id": "template-1",
"name": "template-1",
"order": ["header", "circle", "policy", "footer"]
},
"template-2": {
"id": "template-2",
"name": "template-2",
"order": ["header", "circle", "noffy", "reward", "policy", "footer"]
}
}

View File

@@ -0,0 +1,19 @@
import { createSlice } from '@reduxjs/toolkit';
import initialState from './global.json';
export const Global = createSlice({
name: 'global',
initialState: {
value: initialState
},
reducers: {
updateGlobal: (state, newState) => {
state.value = newState.payload;
},
},
})
// Action creators are generated for each case reducer function
export const { updateGlobal } = Global.actions
export default Global.reducer

View File

@@ -0,0 +1,82 @@
{
"image-1": {
"id": "image-1",
"name": "image-1.jpg",
"dataURL": "https://picsum.photos/seed/1/600",
"size": 102400,
"type": "image/jpeg",
"time": 1700833415517
},
"image-2": {
"id": "image-2",
"name": "image-2.jpg",
"dataURL": "https://picsum.photos/seed/2/600",
"size": 102400,
"type": "image/jpeg",
"time": 1700833415517
},
"image-3": {
"id": "image-3",
"name": "image-3.jpg",
"dataURL": "https://picsum.photos/seed/3/600",
"size": 102400,
"type": "image/jpeg",
"time": 1700833415517
},
"image-4": {
"id": "image-4",
"name": "image-4.jpg",
"dataURL": "https://picsum.photos/seed/4/600",
"size": 102400,
"type": "image/jpeg",
"time": 1700833415517
},
"image-5": {
"id": "image-5",
"name": "image-5.jpg",
"dataURL": "https://picsum.photos/seed/5/600",
"size": 102400,
"type": "image/jpeg",
"time": 1700833415517
},
"image-6": {
"id": "image-6",
"name": "image-6.jpg",
"dataURL": "https://picsum.photos/seed/6/600",
"size": 102400,
"type": "image/jpeg",
"time": 1700833415517
},
"image-7": {
"id": "image-7",
"name": "image-7.jpg",
"dataURL": "https://picsum.photos/seed/7/600",
"size": 102400,
"type": "image/jpeg",
"time": 1700833415517
},
"image-8": {
"id": "image-8",
"name": "image-8.jpg",
"dataURL": "https://picsum.photos/seed/8/600",
"size": 102400,
"type": "image/jpeg",
"time": 1700833415517
},
"image-9": {
"id": "image-9",
"name": "image-9.jpg",
"dataURL": "https://picsum.photos/seed/9/600",
"size": 102400,
"type": "image/jpeg",
"time": 1700833415517
},
"image-10": {
"id": "image-10",
"name": "image-10.jpg",
"dataURL": "https://picsum.photos/seed/10/600",
"size": 102400,
"type": "image/jpeg",
"time": 1700833415517
}
}

View File

@@ -0,0 +1,19 @@
import { createSlice } from '@reduxjs/toolkit';
import initialImages from './images.json';
export const Images = createSlice({
name: 'images',
initialState: {
value: initialImages
},
reducers: {
updateImages: (state, newState) => {
state.value = newState.payload;
},
},
})
// Action creators are generated for each case reducer function
export const { updateImages } = Images.actions
export default Images.reducer

View File

@@ -0,0 +1,56 @@
{
"display": {
"id": "display",
"name": "Display",
"value": "inital",
"selects": ["inline", "block", "inline-block", "flex", "inline-flex"],
"units": [],
"options": ["switch"]
},
"width": {
"id": "width",
"name": "Width",
"values": "auto",
"selects": [
"auto",
"inherit",
"initial",
"max-content",
"min-content",
"fit-content"
],
"units": ["percent", "px", "em", "rem"],
"options": ["slider-range"]
},
"height": {
"id": "height",
"name": "Height",
"values": "auto",
"selects": [
"auto",
"inherit",
"initial",
"max-content",
"min-content",
"fit-content"
],
"units": ["percent", "px", "em", "rem"],
"options": ["slider-range"]
},
"color": {
"id": "color",
"name": "Color",
"values": "#ff0000",
"selects": [],
"units": ["hex"],
"options": ["color-pick"]
},
"background": {
"id": "background",
"name": "Background",
"values": "#ff0000",
"selects": ["color", "gradient", "image"],
"units": ["hex", "url"],
"options": ["color-pick", "upload"]
}
}

View File

@@ -0,0 +1,10 @@
import { configureStore } from '@reduxjs/toolkit';
import Database from './database';
import Images from './images';
export default configureStore({
reducer: {
database: Database,
images: Images
},
})

View File

@@ -0,0 +1,79 @@
import { useState, memo } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { updateDatabase } from '../database/database'
import styled from 'styled-components'
import ImageGallery from './ImageGallery'
const Heading = styled.h3`
font-size: 18px;
line-height: 24px;
text-align: center;
padding: 10px;
`
const Item = styled.div`
padding: 10px;
`
const Title = styled.h4`
font-size: 16px;
line-height: 20px;
font-weight: 500;
margin-bottom: 8px;
`
function Attribute({ id, props }) {
const database = useSelector((state) => state.database.value);
const dispatch = useDispatch();
const AttributeInner = memo(function AttributeInner({ props }) {
const key = props.key;
const type = database[id].type;
if (type === "image" && key === "width" || type === "image" && key === "height") return;
const [value, setValue] = useState(props.value);
const changeHandle = (evt) => {
setValue(evt.target.value);
}
const blurHandle = () => {
const initalValue = database[id].attributes[key];
if (value === initalValue) return;
const newAttributes = { ...database[id].attributes, [key]: value };
const newParams = { ...database[id], attributes: newAttributes };
const newDatabase = { ...database, [id]: newParams };
dispatch(updateDatabase(newDatabase));
}
return (
<Item>
<Title>[ {props.key} ]</Title>
<input className='w-100' type='text' value={value} onChange={changeHandle} onBlur={blurHandle} />
<img src={props.value} alt="Preview" width={"100"} height={"auto"} />
{key === "src" && <ImageGallery id={id} />}
</Item>
)
});
const attributeList = Object.keys(props);
return (
<>
<Heading>Attribute</Heading>
{
attributeList.map((key, index) => {
const attr = {
key: key,
value: props[key]
}
return <AttributeInner props={attr} key={index} />
})
}
</>
)
}
export default Attribute

View File

@@ -0,0 +1,102 @@
import { React } from "react";
import { useSelector, useDispatch } from 'react-redux';
import { updateImages } from '../../database/images';
import styled from 'styled-components';
import ImageEditor from '@toast-ui/react-image-editor';
import 'tui-image-editor/dist/tui-image-editor.css';
const Container = styled.div`
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
`;
const Block = styled.div`
position: relative;
`;
const Button = styled.button`
position: absolute;
top: 12px;
right: 12px;
width: 40px;
height: 40px;
font-size: 20px;
background: red;
color: #fff;
border: 0;
cursor: pointer;
z-index: 9;
&.save {
top: unset;
bottom: 12px;
background: green;
}
`;
function ImageEditors({ image, show, setShow }) {
if (image === null) return;
const dispatch = useDispatch();
const state = useSelector((state) => state.images.value);
const imageEditorsSave = () => {
const images = state;
const canvas = document.querySelector(".lower-canvas");
const dataURL = canvas.toDataURL(image.type);
const newImage = { ...image, 'dataURL': dataURL, 'time': Date.now() };
const newState = { ...images, [image.id]: newImage }
dispatch(updateImages(newState));
setShow(false);
}
const imageEditorsClose = () => {
setShow(false);
}
return (
<>
{show &&
<Container >
<Block>
<ImageEditor
includeUI={{
loadImage: {
path: image.dataURL,
name: 'Edited image',
},
menu: ['shape', 'filter', 'text', 'crop', 'flip', 'rotate', 'draw', 'icon', 'mask'],
uiSize: {
width: '1200px',
height: '600px',
},
menuBarPosition: 'right',
}}
cssMaxHeight={500}
cssMaxWidth={700}
selectionStyle={{
cornerSize: 20,
rotatingPointOffset: 70,
}}
usageStatistics={true}
/>
<Button type="button" onClick={imageEditorsClose}>X</Button>
<Button type="button" className="save" onClick={imageEditorsSave}></Button>
</Block>
</Container>
}
</>
)
}
export default ImageEditors

View File

@@ -0,0 +1,231 @@
import { React, useState } from "react";
import { Tooltip } from 'react-tooltip';
import { useSelector, useDispatch } from 'react-redux';
import { updateImages } from '../../database/images';
import { updateDatabase } from '../../database/database';
import styled from 'styled-components';
import ImageUploading from 'react-images-uploading';
import ImageEditors from "./ImageEditor";
const Container = styled.div`
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 999;
`;
const Box = styled.div`
position: relative;
max-width: 1000px;
`;
const Close = styled.button`
position: absolute;
top: 0;
right: 0;
width: 40px;
height: 40px;
background-color: red;
color: #fff;
border: 0;
z-index: 1;
font-size: 18px;
font-weight: 700;
cursor: pointer;
`;
const Button = styled.button`
color: #fff;
pointer-events: ${(props) => props.$enable ? 'unset' : 'none'};
`;
function ImageUpload({ id, setVisible }) {
const state = useSelector((state) => state.images.value);
const database = useSelector((state) => state.database.value);
const images = Object.entries(state).map(img => img[1]);
const dispatch = useDispatch();
const [show, setShow] = useState(false);
const [editImage, setEditImage] = useState(null);
const [choseImage, setChoseImage] = useState(false);
const maxNumber = 20;
const hideModal = () => {
setVisible(false);
setChoseImage(false);
document.querySelector('body').style.overflow = 'auto';
}
const onChange = (imageList, addUpdateIndex) => {
const newImages = imageList.map(img => {
const id = img.id ? img.id : 'image-' + img.file.lastModified;
const image = img.id ? img : {
'id': id,
'name': img.file.name,
'dataURL': img.dataURL,
'size': img.file.size,
'type': img.file.type,
'time': img.file.lastModified
};
return [id, image];
})
const newImageList = Object.fromEntries(newImages);
dispatch(updateImages(newImageList));
const imagesTotal = Object.keys(newImageList).length;
if (addUpdateIndex && addUpdateIndex[0] + 1 === imagesTotal) {
setTimeout(function () {
const target = document.querySelector('.ld-image-upload-item:last-child');
target.scrollIntoView({ behavior: "smooth", block: "end", inline: "end" });
}, 100)
}
}
const editImages = (index) => {
setEditImage(images[index]);
setShow(true);
}
const onSelect = (index) => {
const target = document.querySelector(`.ld-image-upload-img[data-index='${index}']`);
if (target.classList.contains('selected')) {
target.classList.remove('selected');
setChoseImage(false);
return;
}
document.querySelectorAll('.ld-image-upload-img').forEach(img => {
img.classList.remove("selected");
});
target.classList.add("selected");
setChoseImage(images[index].dataURL);
}
const onSubmit = () => {
const target = database[id];
const targetAttrs = target.attributes;
const targetStyles = target.styles;
const targetImage = target.type === "image";
const newParams = targetImage ? { ...targetAttrs, src: choseImage } : { ...targetStyles, background: `url(${choseImage})` };
const newData = targetImage ? { ...target, attributes: newParams } : { ...target, styles: newParams };
const newDatabase = { ...database, [id]: newData };
dispatch(updateDatabase(newDatabase));
hideModal();
}
return (
<>
<Container>
<Box>
<Close onClick={hideModal}>X</Close>
<ImageUploading multiple value={images} onChange={onChange} maxNumber={maxNumber} dataURLKey="dataURL" >
{({
imageList,
onImageUpload,
onImageRemoveAll,
onImageUpdate,
onImageRemove,
isDragging,
dragProps,
}) => (
<div className="ld-image-upload-container">
<div>
<div className="ld-image-upload-zone" style={isDragging ? { color: 'red' } : undefined} onClick={onImageUpload} {...dragProps}>
<svg viewBox="0 0 1024 1024" focusable="false" data-icon="inbox" width="2em" height="2em" fill="currentColor" aria-hidden="true">
<path d="M885.2 446.3l-.2-.8-112.2-285.1c-5-16.1-19.9-27.2-36.8-27.2H281.2c-17 0-32.1 11.3-36.9 27.6L139.4 443l-.3.7-.2.8c-1.3 4.9-1.7 9.9-1 14.8-.1 1.6-.2 3.2-.2 4.8V830a60.9 60.9 0 0060.8 60.8h627.2c33.5 0 60.8-27.3 60.9-60.8V464.1c0-1.3 0-2.6-.1-3.7.4-4.9 0-9.6-1.3-14.1zm-295.8-43l-.3 15.7c-.8 44.9-31.8 75.1-77.1 75.1-22.1 0-41.1-7.1-54.8-20.6S436 441.2 435.6 419l-.3-15.7H229.5L309 210h399.2l81.7 193.3H589.4zm-375 76.8h157.3c24.3 57.1 76 90.8 140.4 90.8 33.7 0 65-9.4 90.3-27.2 22.2-15.6 39.5-37.4 50.7-63.6h156.5V814H214.4V480.1z"></path>
</svg>
Click or Drop here
</div>
</div>
<div>
<div className="ld-image-upload-list">
{!imageList.length && <div className="ld-image-upload-list__empty"><p><b>Thư viện ảnh trống.</b></p> Vui lòng tải thêm ảnh lên để sử dụng.</div>}
{imageList && imageList.map((image, index) => (
<div className="ld-image-upload-item" key={index}>
<div className="ld-image-upload-img" data-index={index} onClick={() => onSelect(index)}>
<div className="ld-img-block">
<img src={image['dataURL']} alt="" width="140" className="ld-img-content" />
</div>
</div>
<div className="ld-image-upload-wrapper">
<button
type="button"
className="ld-image-upload-btn ld-image-upload-btn__tool"
onClick={() => editImages(index)}
data-tooltip-id="ld-images-tooltip"
data-tooltip-content="Xem, chỉnh sửa"
data-tooltip-place="top"
>
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" viewBox="0 0 512 512">
<path d="M441 58.9L453.1 71c9.4 9.4 9.4 24.6 0 33.9L424 134.1 377.9 88 407 58.9c9.4-9.4 24.6-9.4 33.9 0zM209.8 256.2L344 121.9 390.1 168 255.8 302.2c-2.9 2.9-6.5 5-10.4 6.1l-58.5 16.7 16.7-58.5c1.1-3.9 3.2-7.5 6.1-10.4zM373.1 25L175.8 222.2c-8.7 8.7-15 19.4-18.3 31.1l-28.6 100c-2.4 8.4-.1 17.4 6.1 23.6s15.2 8.5 23.6 6.1l100-28.6c11.8-3.4 22.5-9.7 31.1-18.3L487 138.9c28.1-28.1 28.1-73.7 0-101.8L474.9 25C446.8-3.1 401.2-3.1 373.1 25zM88 64C39.4 64 0 103.4 0 152V424c0 48.6 39.4 88 88 88H360c48.6 0 88-39.4 88-88V312c0-13.3-10.7-24-24-24s-24 10.7-24 24V424c0 22.1-17.9 40-40 40H88c-22.1 0-40-17.9-40-40V152c0-22.1 17.9-40 40-40H200c13.3 0 24-10.7 24-24s-10.7-24-24-24H88z" />
</svg>
</button>
<button
type="button"
className="ld-image-upload-btn ld-image-upload-btn__tool"
onClick={() => onImageUpdate(index)}
data-tooltip-id="ld-images-tooltip"
data-tooltip-content="Thay thế"
data-tooltip-place="top"
>
<svg xmlns="http://www.w3.org/2000/svg" height="14" width="17.5" viewBox="0 0 640 512">
<path d="M144 480C64.5 480 0 415.5 0 336c0-62.8 40.2-116.2 96.2-135.9c-.1-2.7-.2-5.4-.2-8.1c0-88.4 71.6-160 160-160c59.3 0 111 32.2 138.7 80.2C409.9 102 428.3 96 448 96c53 0 96 43 96 96c0 12.2-2.3 23.8-6.4 34.6C596 238.4 640 290.1 640 352c0 70.7-57.3 128-128 128H144zm79-217c-9.4 9.4-9.4 24.6 0 33.9s24.6 9.4 33.9 0l39-39V392c0 13.3 10.7 24 24 24s24-10.7 24-24V257.9l39 39c9.4 9.4 24.6 9.4 33.9 0s9.4-24.6 0-33.9l-80-80c-9.4-9.4-24.6-9.4-33.9 0l-80 80z" />
</svg>
</button>
<button
type="button"
className="ld-image-upload-btn ld-image-upload-btn__tool"
onClick={() => { if (confirm('Are you sure?')) { onImageRemove(index) } }}
data-tooltip-id="ld-images-tooltip"
data-tooltip-content="Xóa"
data-tooltip-place="top"
>
<svg xmlns="http://www.w3.org/2000/svg" height="14" width="12.25" viewBox="0 0 448 512">
<path d="M170.5 51.6L151.5 80h145l-19-28.4c-1.5-2.2-4-3.6-6.7-3.6H177.1c-2.7 0-5.2 1.3-6.7 3.6zm147-26.6L354.2 80H368h48 8c13.3 0 24 10.7 24 24s-10.7 24-24 24h-8V432c0 44.2-35.8 80-80 80H112c-44.2 0-80-35.8-80-80V128H24c-13.3 0-24-10.7-24-24S10.7 80 24 80h8H80 93.8l36.7-55.1C140.9 9.4 158.4 0 177.1 0h93.7c18.7 0 36.2 9.4 46.6 24.9zM80 128V432c0 17.7 14.3 32 32 32H336c17.7 0 32-14.3 32-32V128H80zm80 64V400c0 8.8-7.2 16-16 16s-16-7.2-16-16V192c0-8.8 7.2-16 16-16s16 7.2 16 16zm80 0V400c0 8.8-7.2 16-16 16s-16-7.2-16-16V192c0-8.8 7.2-16 16-16s16 7.2 16 16zm80 0V400c0 8.8-7.2 16-16 16s-16-7.2-16-16V192c0-8.8 7.2-16 16-16s16 7.2 16 16z" />
</svg>
</button>
</div>
</div>
))}
</div>
<div className="ld-image-upload-wrapper ld-image-upload-wrapper__bottom">
<button type="button" className="ld-image-upload-btn remove-all" onClick={() => { if (confirm('Are you sure?')) { onImageRemoveAll() } }}>Remove all images</button>
<Button $enable={choseImage} type="button" className="ld-image-upload-btn submit" onClick={() => { if (confirm('Are you sure?')) { onSubmit() } }}>Replace</Button>
</div>
</div>
</div>
)}
</ImageUploading>
<ImageEditors image={editImage} show={show} setShow={setShow} />
</Box>
<Tooltip id="ld-images-tooltip" />
</Container>
</>
);
}
export default ImageUpload

View File

@@ -0,0 +1,36 @@
import { React, useState } from 'react'
import { createPortal } from 'react-dom'
import styled from 'styled-components'
import ImageUpload from './ImageUpload'
const Button = styled.button`
font-weight: 500;
background: #fff;
color: #f00;
padding: 8px 16px;
margin: 8px 0 0;
`
function ImageGallery({ id }) {
const [visible, setVisible] = useState(false);
const clickHandle = () => {
setVisible(true);
document.querySelector('body').style.overflow = 'hidden';
}
return (
<>
<Button type="button" onClick={clickHandle}>Chọn ảnh</Button>
{visible && createPortal(
<ImageUpload id={id} setVisible={setVisible} />
,
document.body
)}
</>
);
}
export default ImageGallery

View File

@@ -0,0 +1,66 @@
import { useState } from 'react'
import styled from 'styled-components'
import { ColorPick, ColorGradientPick } from './Options'
import ImageGallery from '../../ImageGallery'
const Select = styled.select`
position: absolute;
top: 10px;
right: 10px;
height: 24px;
`
const Image = styled.img`
display: block;
width: auto;
max-height: 200px;
margin-top: 10px;
`
const BackgroundInner = ({ props, selected }) => {
const { id, value } = props;
switch (selected) {
case 'color':
return (
<ColorPick props={props} />
)
case 'gradient':
return (
<ColorGradientPick props={props} />
)
case 'image':
const image = value.substring(4, value.length - 1);
return (
<>
<Image src={image} alt="Preview" width={"50"} height={"50"} />
<ImageGallery id={id} />
</>
)
default:
return;
}
}
function Background({ props }) {
const selects = props.settings.selects;
const [selected, setSelected] = useState(props.selected);
const valueChange = (evt) => {
setSelected(evt.target.value);
}
return (
<>
<Select value={selected} onChange={valueChange}>
{
selects.map((select, index) => <option value={select} key={index}>{select}</option>)
}
</Select>
<BackgroundInner props={props} selected={selected} />
</>
)
}
export default Background

View File

@@ -0,0 +1,7 @@
import { ColorPick } from './Options';
function Color({ id, props }) {
return <ColorPick id={id} props={props} />
}
export default Color

View File

@@ -0,0 +1,12 @@
import { SwitchButton, SelectField } from './Options';
function Display({ props }) {
return (
<>
<SwitchButton props={props} />
<SelectField props={props} />
</>
)
}
export default Display

View File

@@ -0,0 +1,207 @@
import { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux'
import { updateDatabase } from '../../../database/database'
import { SketchPicker } from 'react-color';
import styled from 'styled-components';
const ColorContainer = styled.div`
position: relative;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
`;
const ColorOverlay = styled.div`
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1;
`;
const ColorPicker = styled.div`
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
z-index: 9;
box-shadow: 0 1px 5px rgba(0,0,0,0.3);
`;
const ColorButton = styled.button`
display: block;
width: 36px;
height: 36px;
margin: 10px;
border: 1px solid #000000;
cursor: pointer;
background-color: ${props => props.$backgroundColor}
`;
const ColorInput = styled.input`
height: 36px;
width: 36px;
font-size: 18px;
text-align: center;
margin: 10px;
`
const BackgroundGradient = styled.div`
width: 100px;
height: 100px;
background: ${props => props.$backgroundGradient};
margin: 10px;
`;
const SwitchButton = ({ props }) => {
const database = useSelector((state) => state.database.value);
const dispatch = useDispatch();
const { id, value, settings, selected } = props;
const valueChange = (e) => {
console.log(id, e.target.checked);
const display = e.target.checked ? "revert-layer" : "none";
console.log(display);
// const key = settings.id;
// const newStyles = { ...database[id].styles, [key]: display };
// const newParams = { ...database[id], styles: newStyles };
// const newDatabase = { ...database, [id]: newParams };
// dispatch(updateDatabase(newDatabase));
}
return (
<label className="ld-switch">
<input className='ld-switch-checkbox' type="checkbox" onChange={valueChange} />
<span className="ld-switch-slider"></span>
</label>
)
}
const SlideRange = ({ props }) => {
const { id, value, settings, selected } = props;
const valueChange = (e) => {
console.log(id, e.target.value)
}
return (
<div className="ld-slide-range">
<input type="range" min="0" max="100" defaultValue="50" className="ld-slide-range-line" onInput={valueChange} />
</div>
)
}
const SelectField = ({ props }) => {
console.log(props);
const { id, value, settings, selected } = props;
const selects = settings.selects;
const handleChange = () => {
}
return (
<select value={selected} onChange={handleChange}>
{
selects.map((select, index) => <option value={select} key={index}>{select}</option>)
}
</select>
)
}
const ColorPick = ({ props, inital, handle }) => {
const database = useSelector((state) => state.database.value);
const dispatch = useDispatch();
const { id, value, settings, selected } = props;
const initalValue = inital ? inital : selected === "color" ? value : "#f00";
const key = settings.id;
const [color, setColor] = useState(initalValue);
const [display, setDisplay] = useState(false);
const valueChange = (color) => {
setColor(color.hex);
}
const valueSave = () => {
setDisplay(!display);
if (handle) {
handle(color);
return;
}
const newStyles = { ...database[id].styles, [key]: color };
const newParams = { ...database[id], styles: newStyles };
const newDatabase = { ...database, [id]: newParams };
dispatch(updateDatabase(newDatabase));
}
const showHandle = () => {
setDisplay(!display);
}
return (
<ColorContainer>
<ColorButton $backgroundColor={color} onClick={showHandle} />
{display &&
<>
<ColorOverlay onClick={valueSave} />
<ColorPicker>
<SketchPicker color={color} onChange={valueChange} />
</ColorPicker>
</>
}
</ColorContainer>
)
}
const ColorGradientPick = ({ props }) => {
const database = useSelector((state) => state.database.value);
const dispatch = useDispatch();
let initalAngel = 0;
let initalColor1 = "#000";
let initalColor2 = "#fff";
const { id, value, settings, selected } = props;
const key = settings.id;
if (selected === "gradient") {
const gradient = value.substring(16, value.length - 1).split(",");
initalAngel = gradient[0].replace("deg", "");
initalColor1 = gradient[1].trim();
initalColor2 = gradient[2].trim();
}
const [angle, setAngle] = useState(initalAngel);
const [colorStart, setColorStart] = useState(initalColor1);
const [colorEnd, setColorEnd] = useState(initalColor2);
const backgroundGradient = `linear-gradient(${angle}deg, ${colorStart}, ${colorEnd})`
const clickHandle = () => {
const newStyles = { ...database[id].styles, [key]: backgroundGradient };
const newParams = { ...database[id], styles: newStyles };
const newDatabase = { ...database, [id]: newParams };
dispatch(updateDatabase(newDatabase));
}
return (
<ColorContainer>
<ColorInput value={angle} onChange={(evt) => setAngle(evt.target.value)} />
<ColorPick props={props} inital={initalColor1} handle={setColorStart} />
<ColorPick props={props} inital={initalColor2} handle={setColorEnd} />
<BackgroundGradient $backgroundGradient={backgroundGradient} onClick={clickHandle} />
</ColorContainer>
)
}
export { SwitchButton, SlideRange, SelectField, ColorPick, ColorGradientPick }

View File

@@ -0,0 +1,12 @@
import { SlideRange, SelectField } from './Options';
const Size = ({ id, props }) => {
return (
<>
<SlideRange id={id} props={props} />
<SelectField id={id} props={props} />
</>
)
}
export default Size

View File

@@ -0,0 +1,43 @@
import Size from './Size'
import Color from './Color'
import Display from './Display'
import Background from './Background'
import properties from '../../../database/properties.json'
const PropertyInner = ({ props }) => {
const type = props.type;
const value = props.value;
const propsNew = {
id: props.id,
value: value,
settings: properties[type],
selected: value
}
switch (type) {
// case 'display':
// return <Display props={propsNew} />
// case 'width':
// return <Size props={propsNew} />
// case 'height':
// return <Size props={propsNew} />
case 'color':
return <Color props={propsNew} />
case 'background':
const selectedBg = type === "background" ? value.indexOf("url") > -1 ? "image" : value.indexOf("gradient") > -1 ? "gradient" : "color" : "color";
const propsBg = { ...propsNew, selected: selectedBg }
return <Background props={propsBg} />
default:
return;
}
}
function Property({ props }) {
return (
<PropertyInner props={props} />
)
}
export default Property

View File

@@ -0,0 +1,83 @@
import { useState, memo } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { updateDatabase } from '../../database/database'
import styled from 'styled-components'
import Property from './Properties'
const Heading = styled.h3`
font-size: 18px;
line-height: 24px;
text-align: center;
padding: 10px;
margin-top: 10px;
`
const Item = styled.div`
position: relative;
padding: 10px;
`
const Title = styled.h4`
font-size: 16px;
line-height: 24px;
font-weight: 500;
margin-bottom: 8px;
`
function StyleManager({ id, props }) {
const database = useSelector((state) => state.database.value);
const dispatch = useDispatch();
const StyleInner = memo(function StyleInner({ props }) {
const key = props.key;
const [value, setValue] = useState(props.value);
const changeHandle = (evt) => {
setValue(evt.target.value);
}
const blurHandle = () => {
const initalValue = database[id].styles[key];
if (value === initalValue) return;
const newStyles = { ...database[id].styles, [key]: value };
const newParams = { ...database[id], styles: newStyles };
const newDatabase = { ...database, [id]: newParams };
dispatch(updateDatabase(newDatabase));
}
const propertyProps = {
id: id,
type: key,
value: value
}
return (
<Item>
<Title>[ {key} ]</Title>
<input className='w-100' type='text' value={value} onChange={changeHandle} onBlur={blurHandle} />
<Property props={propertyProps} />
</Item>
)
});
const styleList = Object.keys(props);
return (
<>
<Heading>Style</Heading>
{
styleList.map((key, index) => {
const attr = {
key: key,
value: props[key]
}
return <StyleInner props={attr} key={index} />
})
}
</>
)
}
export default StyleManager

View File

@@ -0,0 +1,43 @@
import styled from 'styled-components'
import Attribute from './Attribute'
import StyleManager from './StyleManager'
const Container = styled.div`
position: sticky;
top: 0;
width: 300px;
max-height: 100vh;
padding-bottom: 10px;
background: #333;
color : #fff;
overflow: auto;
`
const Title = styled.h2`
font-size: 20px;
line-height: 24px;
text-align: center;
padding: 10px;
border-bottom: 1px solid #f9f9f9;
`
function Editor({ props }) {
const id = props.id;
const attributes = props.attributes;
const styles = props.styles;
const hasAttributes = attributes && Object.keys(attributes).length > 0;
const hasStyles = styles && Object.keys(styles).length > 0;
return (
<>
<Container>
<Title>Editor</Title>
{hasAttributes && <Attribute id={id} props={attributes} />}
{hasStyles && <StyleManager id={id} props={styles} />}
</Container>
</>
)
}
export default Editor

View File

@@ -0,0 +1,734 @@
:root {
--color-primary: #ff74ec;
}
*,
::after,
::before {
padding: 0;
margin: 0;
-webkit-box-sizing: inherit;
box-sizing: inherit;
}
html {
font-size: 14px;
}
body {
-webkit-box-sizing: border-box;
box-sizing: border-box;
font-family: "Chakra Petch", sans-serif;
color: #000;
word-break: break-word;
line-height: calc(100% + 6px);
font-weight: 400;
letter-spacing: 0.15px;
}
a {
display: inline-block;
text-decoration: none;
font-size: inherit;
line-height: inherit;
color: inherit;
}
input,
input::placeholder,
select,
textarea,
textarea::placeholder {
font-family: inherit;
font-size: 1rem;
}
input {
max-width: 100%;
outline-color: var(--color-primary);
}
textarea {
display: inherit;
resize: vertical;
outline-color: var(--color-primary);
}
img {
max-width: 100%;
max-height: 100%;
height: auto;
}
button {
border: 0;
outline: 0;
background: transparent;
cursor: pointer;
font-family: inherit;
}
.row {
display: -ms-flexbox;
display: -webkit-box;
display: flex;
-ms-flex-wrap: wrap;
flex-wrap: wrap;
margin-right: -8px;
margin-left: -8px;
}
.no-gutters {
margin-right: 0;
margin-left: 0;
}
.no-gutters > .col,
.no-gutters > [class*="col-"] {
padding-right: 0;
padding-left: 0;
}
.col,
.col-1,
.col-10,
.col-11,
.col-12,
.col-2,
.col-3,
.col-4,
.col-5,
.col-6,
.col-7,
.col-8,
.col-9,
.col-auto,
.col-sm,
.col-sm-1,
.col-sm-10,
.col-sm-11,
.col-sm-12,
.col-sm-2,
.col-sm-3,
.col-sm-4,
.col-sm-5,
.col-sm-6,
.col-sm-7,
.col-sm-8,
.col-sm-9,
.col-sm-auto {
position: relative;
width: 100%;
padding-right: 8px;
padding-left: 8px;
}
.col {
-ms-flex-preferred-size: 0;
flex-basis: 0;
-ms-flex-positive: 1;
-webkit-box-flex: 1;
flex-grow: 1;
max-width: 100%;
}
.row-cols-1 > * {
-ms-flex: 0 0 100%;
-webkit-box-flex: 0;
flex: 0 0 100%;
max-width: 100%;
}
.row-cols-2 > * {
-ms-flex: 0 0 50%;
-webkit-box-flex: 0;
flex: 0 0 50%;
max-width: 50%;
}
.row-cols-3 > * {
-ms-flex: 0 0 33.333333%;
-webkit-box-flex: 0;
flex: 0 0 33.333333%;
max-width: 33.333333%;
}
.row-cols-4 > * {
-ms-flex: 0 0 25%;
-webkit-box-flex: 0;
flex: 0 0 25%;
max-width: 25%;
}
.row-cols-5 > * {
-ms-flex: 0 0 20%;
-webkit-box-flex: 0;
flex: 0 0 20%;
max-width: 20%;
}
.row-cols-6 > * {
-ms-flex: 0 0 16.666667%;
-webkit-box-flex: 0;
flex: 0 0 16.666667%;
max-width: 16.666667%;
}
.col-auto {
-ms-flex: 0 0 auto;
-webkit-box-flex: 0;
flex: 0 0 auto;
width: auto;
max-width: 100%;
}
.col-1 {
-ms-flex: 0 0 8.333333%;
-webkit-box-flex: 0;
flex: 0 0 8.333333%;
max-width: 8.333333%;
}
.col-2 {
-ms-flex: 0 0 16.666667%;
-webkit-box-flex: 0;
flex: 0 0 16.666667%;
max-width: 16.666667%;
}
.col-3 {
-ms-flex: 0 0 25%;
-webkit-box-flex: 0;
flex: 0 0 25%;
max-width: 25%;
}
.col-4 {
-ms-flex: 0 0 33.333333%;
-webkit-box-flex: 0;
flex: 0 0 33.333333%;
max-width: 33.333333%;
}
.col-5 {
-ms-flex: 0 0 41.666667%;
-webkit-box-flex: 0;
flex: 0 0 41.666667%;
max-width: 41.666667%;
}
.col-6 {
-ms-flex: 0 0 50%;
-webkit-box-flex: 0;
flex: 0 0 50%;
max-width: 50%;
}
.col-7 {
-ms-flex: 0 0 58.333333%;
-webkit-box-flex: 0;
flex: 0 0 58.333333%;
max-width: 58.333333%;
}
.col-8 {
-ms-flex: 0 0 66.666667%;
-webkit-box-flex: 0;
flex: 0 0 66.666667%;
max-width: 66.666667%;
}
.col-9 {
-ms-flex: 0 0 75%;
-webkit-box-flex: 0;
flex: 0 0 75%;
max-width: 75%;
}
.col-10 {
-ms-flex: 0 0 83.333333%;
-webkit-box-flex: 0;
flex: 0 0 83.333333%;
max-width: 83.333333%;
}
.col-11 {
-ms-flex: 0 0 91.666667%;
-webkit-box-flex: 0;
flex: 0 0 91.666667%;
max-width: 91.666667%;
}
.col-12 {
-ms-flex: 0 0 100%;
-webkit-box-flex: 0;
flex: 0 0 100%;
max-width: 100%;
}
.offset-1 {
margin-left: 8.333333%;
}
.offset-2 {
margin-left: 16.666667%;
}
.offset-3 {
margin-left: 25%;
}
.offset-4 {
margin-left: 33.333333%;
}
.offset-5 {
margin-left: 41.666667%;
}
.offset-6 {
margin-left: 50%;
}
.offset-7 {
margin-left: 58.333333%;
}
.offset-8 {
margin-left: 66.666667%;
}
.offset-9 {
margin-left: 75%;
}
.offset-10 {
margin-left: 83.333333%;
}
.offset-11 {
margin-left: 91.666667%;
}
@media (max-width: 1600px) {
.grid {
gap: 12px;
}
.row {
margin-right: -6px;
margin-left: -6px;
}
.col,
.col-1,
.col-10,
.col-11,
.col-12,
.col-2,
.col-3,
.col-4,
.col-5,
.col-6,
.col-7,
.col-8,
.col-9,
.col-auto,
.col-sm,
.col-sm-1,
.col-sm-10,
.col-sm-11,
.col-sm-12,
.col-sm-2,
.col-sm-3,
.col-sm-4,
.col-sm-5,
.col-sm-6,
.col-sm-7,
.col-sm-8,
.col-sm-9,
.col-sm-auto {
padding-right: 6px;
padding-left: 6px;
}
.no-gutters {
margin-right: 0;
margin-left: 0;
}
}
@media (max-width: 992px) {
.grid {
gap: 10px;
}
.row {
margin-right: -5px;
margin-left: -5px;
}
.col,
.col-1,
.col-10,
.col-11,
.col-12,
.col-2,
.col-3,
.col-4,
.col-5,
.col-6,
.col-7,
.col-8,
.col-9,
.col-auto,
.col-sm,
.col-sm-1,
.col-sm-10,
.col-sm-11,
.col-sm-12,
.col-sm-2,
.col-sm-3,
.col-sm-4,
.col-sm-5,
.col-sm-6,
.col-sm-7,
.col-sm-8,
.col-sm-9,
.col-sm-auto {
padding-right: 5px;
padding-left: 5px;
}
.col-sm-1 {
-ms-flex: 0 0 8.333333%;
-webkit-box-flex: 0;
flex: 0 0 8.333333%;
max-width: 8.333333%;
}
.col-sm-2 {
-ms-flex: 0 0 16.666667%;
-webkit-box-flex: 0;
flex: 0 0 16.666667%;
max-width: 16.666667%;
}
.col-sm-3 {
-ms-flex: 0 0 25%;
-webkit-box-flex: 0;
flex: 0 0 25%;
max-width: 25%;
}
.col-sm-4 {
-ms-flex: 0 0 33.333333%;
-webkit-box-flex: 0;
flex: 0 0 33.333333%;
max-width: 33.333333%;
}
.col-sm-5 {
-ms-flex: 0 0 41.666667%;
-webkit-box-flex: 0;
flex: 0 0 41.666667%;
max-width: 41.666667%;
}
.col-sm-6 {
-ms-flex: 0 0 50%;
-webkit-box-flex: 0;
flex: 0 0 50%;
max-width: 50%;
}
.col-sm-7 {
-ms-flex: 0 0 58.333333%;
-webkit-box-flex: 0;
flex: 0 0 58.333333%;
max-width: 58.333333%;
}
.col-sm-8 {
-ms-flex: 0 0 66.666667%;
-webkit-box-flex: 0;
flex: 0 0 66.666667%;
max-width: 66.666667%;
}
.col-sm-9 {
-ms-flex: 0 0 75%;
-webkit-box-flex: 0;
flex: 0 0 75%;
max-width: 75%;
}
.col-sm-10 {
-ms-flex: 0 0 83.333333%;
-webkit-box-flex: 0;
flex: 0 0 83.333333%;
max-width: 83.333333%;
}
.col-sm-11 {
-ms-flex: 0 0 91.666667%;
-webkit-box-flex: 0;
flex: 0 0 91.666667%;
max-width: 91.666667%;
}
.col-sm-12 {
-ms-flex: 0 0 100%;
-webkit-box-flex: 0;
flex: 0 0 100%;
max-width: 100%;
}
.offset-sm-0 {
margin-left: 0;
}
.no-gutters {
margin-right: 0;
margin-left: 0;
}
}
.pulse-icon {
position: absolute;
display: inline-block;
top: 10px;
left: 10px;
}
.pulse-icon .icon {
width: 8px;
height: 8px;
text-align: center;
display: inline-block;
border-radius: 8px;
color: #00e745;
background: #00e745;
position: absolute;
top: 3px;
left: 3px;
}
.pulse-icon .elements {
position: absolute;
top: 0;
left: 0;
z-index: 1;
}
.pulse-icon .pulse {
position: absolute;
-webkit-animation: pulse-wave 1s linear infinite both;
animation: pulse-wave 1s linear infinite both;
border-radius: 50%;
border: solid 1px rgb(0, 231, 69, 0.5);
width: 14px;
height: 14px;
top: 0;
left: 0;
}
@keyframes pulse-wave {
0% {
opacity: 0;
-webkit-transform: scale(1);
transform: scale(1);
}
50% {
opacity: 1;
-webkit-transform: scale(1.5);
transform: scale(1.5);
}
100% {
opacity: 0;
-webkit-transform: scale(2);
transform: scale(2);
}
}
.mb-display {
display: block;
}
.pc-display {
display: none;
}
.flex-1 {
flex: 1;
}
.w-100 {
width: 100%;
}
.list {
list-style: none;
}
.ld-item {
outline: 0;
transition: all 0.3s;
}
.ld-item:hover {
box-shadow: inset 0 0 3px #000;
}
.tui-image-editor-container .tui-image-editor-main {
top: 0;
}
.tui-image-editor-container .tui-image-editor-header {
display: none;
}
.tui-image-editor-container .tui-image-editor-help-menu.left {
height: auto;
}
.tui-image-editor-container [tooltip-content="Delete"],
.tui-image-editor-container [tooltip-content="DeleteAll"] {
display: none !important;
}
.ld-img-block {
position: relative;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
padding-bottom: 100%;
}
.ld-img-content {
position: absolute;
bottom: 0;
right: 0;
top: 0;
left: 0;
margin: auto auto;
}
.ld-img-content.object-cover {
width: 100%;
height: 100%;
-o-object-fit: cover;
object-fit: cover;
}
.ld-block:hover .ld-block-tool,
.ld-element:hover .ld-element-tool {
display: inline-flex;
}
.ld-element__resize {
border: 1px dashed lightgray;
}
.ld-element__resize:hover {
border-color: red;
}
.ld-element__image {
display: inline-block;
}
.ld-switch {
position: relative;
display: inline-block;
width: 30px;
height: 16px;
}
.ld-switch-checkbox {
opacity: 0;
width: 0;
height: 0;
}
.ld-switch-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
border-radius: 16px;
-webkit-transition: 0.4s;
transition: 0.4s;
}
.ld-switch-slider:before {
position: absolute;
content: "";
height: 10px;
width: 10px;
left: 4px;
bottom: 3px;
background-color: white;
border-radius: 50%;
-webkit-transition: 0.4s;
transition: 0.4s;
}
.ld-switch-checkbox:checked + .ld-switch-slider {
background-color: #2196f3;
}
.ld-switch-checkbox:focus + .ld-switch-slider {
box-shadow: 0 0 1px #2196f3;
}
.ld-switch-checkbox:checked + .ld-switch-slider:before {
-webkit-transform: translateX(12px);
-ms-transform: translateX(12px);
transform: translateX(12px);
}
.ld-slide-range {
width: 100px;
}
.ld-slide-range-line {
-webkit-appearance: none;
appearance: none;
width: 100%;
height: 10px;
background: #d3d3d3;
border-radius: 5px;
outline: 0;
opacity: 0.7;
-webkit-transition: 0.2s;
transition: opacity 0.2s;
}
.ld-slide-range-line:hover {
opacity: 1;
}
.ld-slide-range-line::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 14px;
height: 14px;
background: #2196f3;
border-radius: 50%;
cursor: pointer;
}
.ld-slide-range-line::-moz-range-thumb {
width: 14px;
height: 14px;
background: #2196f3;
border-radius: 50%;
cursor: pointer;
}
.ld-image-upload-container {
display: flex;
gap: 40px;
background: #fff;
padding: 40px;
}
.ld-image-upload-list {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0 20px;
width: 520px;
max-height: 419px;
min-height: 419px;
overflow: auto;
padding: 20px 20px 0;
border: 2px dashed #f0f0f0;
}
.ld-image-upload-list__empty {
grid-column: span 3;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
}
.ld-image-upload-item {
padding-bottom: 20px;
}
.ld-image-upload-img {
transition: padding 0.5s;
}
.ld-image-upload-img.selected {
padding: 10px;
border: 2px solid red;
}
.ld-image-upload-btn {
border: 0;
padding: 10px;
border-radius: 5px;
cursor: pointer;
}
.ld-image-upload-btn.remove-all {
background: red;
color: #fff;
}
.ld-image-upload-btn.submit {
background: green;
color: #fff;
}
.ld-image-upload-btn__tool {
padding: 0;
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
}
.ld-image-upload-zone {
cursor: pointer;
text-align: center;
padding: 40px 20px;
border: 2px dashed #f0f0f0;
}
.ld-image-upload-zone svg {
display: block;
margin: 0 auto 8px;
}
.ld-image-upload-wrapper {
display: flex;
justify-content: center;
gap: 8px;
margin-top: 10px;
}
.ld-image-upload-wrapper__bottom {
justify-content: space-between;
}

View File

@@ -0,0 +1,15 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { Provider } from 'react-redux';
import store from './database/store.jsx';
import App from './App.jsx'
import './index.css'
createRoot(document.getElementById('root')).render(
<StrictMode>
<Provider store={store}>
<App />
</Provider>
</StrictMode>,
)

View File

@@ -0,0 +1,16 @@
function Circle() {
return (
<div className="container">
<div className="circle">
<img id="circle_background" className="ld-item circle-bg" src="src/assets/images/hnc-game-2-cricle-1.png" alt="Cricle 1" width="493" height="493" />
<img className="circle-item rw" src="src/assets/images/hnc-game-2-cricle-2.png" alt="Cricle 1" width="406" height="406" />
<img id="circle_arrow" className="ld-item circle-item ar" src="src/assets/images/hnc-game-2-cricle-3.png" alt="Cricle 1" width="62" height="133" />
<button className="circle-item btn" type="button" onclick="funcTest1();">
<img id="circle_button" className="ld-item" src="src/assets/images/hnc-game-2-cricle-4.png" alt="Cricle 1" width="162" height="162" />
</button>
</div>
</div>
)
}
export default Circle

View File

@@ -0,0 +1,11 @@
import Editable from "../components/Editable"
function Footer({ props }) {
return (
<>
<Editable props={props.footer} />
</>
)
}
export default Footer

View File

@@ -0,0 +1,24 @@
import Image from "../components/Image"
import Editable from "../components/Editable"
function Header({ props }) {
return (
<>
<header className="header">
<div className="container">
<div>
<Image props={props.logo} />
</div>
<div>
<Image props={props.banner} />
</div>
<Editable props={props.date} />
</div>
</header>
</>
)
}
export default Header

View File

@@ -0,0 +1,28 @@
function Noffy() {
return (
<div className="container">
<div className="noffy">
<div className="noffy-remain">
Bạn <b className="noffy-number">2</b> lượt quay
</div>
<div className="noffy-player">
<p>
<span className="noffy-pluse"></span>
<span className="pulse-icon">
<span className="icon"></span>
<span className="elements">
<span className="pulse"></span>
</span>
</span>
Đang online: <b className="noffy-number">168</b>
</p>
<p>Đã chơi: <b className="noffy-number">403</b></p>
<p>Đang chơi: <b className="noffy-number">16</b></p>
</div>
</div>
</div>
)
}
export default Noffy

View File

@@ -0,0 +1,25 @@
import Editable from "../components/Editable"
function Policy({ props }) {
return (
<div className="container">
<div className="box policy">
<Editable props={props.policy_heading} />
<Editable props={props.policy_content} />
</div>
<div className="hotline">
<a href="tel:19001903" className="tel">
<img src="src/assets/images/hnc-game-2-icon-phone-1.png" alt="Phone" width="26" height="26" />
<span>
Hotline:
<Editable props={props.policy_hotline} />
</span>
</a>
</div>
</div>
)
}
export default Policy

View File

@@ -0,0 +1,41 @@
import styled from "styled-components";
import Editable from "../components/Editable"
const StyledReward = styled.div`
${props => props.$sty}
`
function Reward({ props }) {
const limit = parseInt(props.reward.attributes.limit);
const style = props.reward.styles;
return (
<div className="container">
<StyledReward id="reward" className="ld-item box reward" $sty={style}>
<Editable props={props.reward_heading} />
<ul className=" list reward-list">
{
[...Array(limit)].map((item, index) =>
<li className="reward-item" key={index}>
<div className="reward-top">
<div className="reward-img">
<img src="src/assets/images/hnc-game-2-voucher-1.png" alt="Voucher" width="111" height="111" />
</div>
<div className="flex-1">
<p className="reward-no">1 giải nhất</p>
<p className="reward-name">PC GAMING HACOM HURACAN</p>
</div>
</div>
<p className="reward-price">Trị giá <b className="reward-bold">59.999.000 đ</b></p>
</li>
)
}
</ul>
</StyledReward>
</div>
)
}
export default Reward

View File

@@ -0,0 +1,116 @@
import { useState } from 'react'
import styled from 'styled-components'
import Header from './Header'
import Footer from './Footer'
import Policy from './Policy'
import Reward from './Reward'
import Noffy from './Noffy'
import Circle from './Circle'
import global from '../database/global.json'
const Container = styled.div`
position: relative;
flex: 1;
min-height: 100vh;
padding-top: 45px;
${props => props.$background}
`
const Popup = styled.div`
position: absolute;
top: 50%;
left: 50%;
padding: 50px;
transform: translate(-50%, -50%);
z-index: 9;
background: #f5f5f5;
`
const Button = styled.button`
padding: 6px 12px;
background: red;
color: #fff;
margin: 10px;
`
const Navbar = styled.div`
position: absolute;
top: 0;
left: 0;
right: 0;
height: 45px;
background: #333;
z-index: 9;
`
function Template({ setTarget, props }) {
const [template, setTemplate] = useState("");
const id = props.main.id;
const className = props.main.className;
const background = `
background: ${props.main.styles.background};
background-repeat: ${props.main.styles.backgroundRepeat};
background-size: ${props.main.styles.backgroundSize};
`
const clickHandle = (evt) => {
const target = evt.target;
const isItem = target.classList.contains("ld-item");
if (isItem) {
setTarget(target.id);
}
}
return (
<>
{template ?
<Container id={id} className={'ld-item ' + className} $background={background} onClick={clickHandle}>
<Navbar>
<Button onClick={() => setTemplate("")}>Chọn Template</Button>
</Navbar>
{
global[template].order.map((type, index) => {
switch (type) {
case 'header':
return <Header props={props} key={index} />
case 'circle':
return <Circle props={props} key={index} />
case 'noffy':
return <Noffy props={props} key={index} />
case 'reward':
return <Reward props={props} key={index} />
case 'policy':
return <Policy props={props} key={index} />
case 'footer':
return <Footer props={props} key={index} />
default:
return;
}
})
}
</Container>
:
<Container>
<Popup>
Chọn Template:
{
Object.keys(global).map((id, index) =>
<Button key={index} onClick={() => setTemplate(id)}>{global[id].name}</Button>
)
}
</Popup>
</Container>
}
</>
)
}
export default Template

View File

@@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
})