mirror of
https://github.com/chinchang/web-maker.git
synced 2025-07-27 08:40:10 +02:00
67
gulpfile.js
67
gulpfile.js
@@ -13,6 +13,7 @@ const merge = require('merge-stream');
|
||||
// const zip = require('gulp-zip');
|
||||
var packageJson = JSON.parse(fs.readFileSync('./package.json'));
|
||||
const connect = require('gulp-connect');
|
||||
const APP_FOLDER = 'create';
|
||||
|
||||
function minifyJs(fileName) {
|
||||
const content = fs.readFileSync(fileName, 'utf8');
|
||||
@@ -44,47 +45,54 @@ gulp.task('copyFiles', function () {
|
||||
return merge(
|
||||
gulp
|
||||
.src('src/lib/codemirror/theme/*')
|
||||
.pipe(gulp.dest('app/lib/codemirror/theme')),
|
||||
.pipe(gulp.dest(`${APP_FOLDER}/lib/codemirror/theme`)),
|
||||
gulp
|
||||
.src('src/lib/codemirror/mode/**/*')
|
||||
.pipe(gulp.dest('app/lib/codemirror/mode')),
|
||||
gulp.src('src/lib/transpilers/*').pipe(gulp.dest('app/lib/transpilers')),
|
||||
gulp.src('src/lib/prettier-worker.js').pipe(gulp.dest('app/lib/')),
|
||||
gulp.src('src/lib/prettier/*').pipe(gulp.dest('app/lib/prettier')),
|
||||
.pipe(gulp.dest(`${APP_FOLDER}/lib/codemirror/mode`)),
|
||||
gulp
|
||||
.src('src/lib/transpilers/*')
|
||||
.pipe(gulp.dest(`${APP_FOLDER}/lib/transpilers`)),
|
||||
gulp
|
||||
.src('src/lib/prettier-worker.js')
|
||||
.pipe(gulp.dest(`${APP_FOLDER}/lib/`)),
|
||||
gulp
|
||||
.src('src/lib/prettier/*')
|
||||
.pipe(gulp.dest(`${APP_FOLDER}/lib/prettier`)),
|
||||
gulp
|
||||
.src(['!src/lib/monaco/monaco.bundle.js', 'src/lib/monaco/**/*'])
|
||||
.pipe(gulp.dest('app/lib/monaco')),
|
||||
gulp.src('src/lib/screenlog.js').pipe(gulp.dest('app/lib')),
|
||||
gulp.src('icons/*').pipe(gulp.dest('app/icons')),
|
||||
gulp.src('src/assets/*').pipe(gulp.dest('app/assets')),
|
||||
gulp.src('src/templates/*').pipe(gulp.dest('app/templates')),
|
||||
gulp.src('preview/*').pipe(gulp.dest('app/preview')),
|
||||
.pipe(gulp.dest(`${APP_FOLDER}/lib/monaco`)),
|
||||
gulp.src('src/lib/screenlog.js').pipe(gulp.dest(`${APP_FOLDER}/lib`)),
|
||||
gulp.src('icons/*').pipe(gulp.dest(`${APP_FOLDER}/icons`)),
|
||||
gulp.src('src/assets/*').pipe(gulp.dest(`${APP_FOLDER}/assets`)),
|
||||
gulp.src('src/templates/*').pipe(gulp.dest(`${APP_FOLDER}/templates`)),
|
||||
gulp.src('preview/*').pipe(gulp.dest(`${APP_FOLDER}/preview`)),
|
||||
gulp
|
||||
.src([
|
||||
'src/preview.html',
|
||||
'src/indexpm.html',
|
||||
'src/detached-window.js',
|
||||
'src/icon-48.png',
|
||||
'src/icon-128.png',
|
||||
'src/manifest.json'
|
||||
])
|
||||
.pipe(gulp.dest('app')),
|
||||
.pipe(gulp.dest(APP_FOLDER)),
|
||||
|
||||
gulp.src('build/*').pipe(gulp.dest('app')),
|
||||
gulp.src('build/*').pipe(gulp.dest(APP_FOLDER)),
|
||||
|
||||
// Following CSS are copied to build/ folder where they'll be referenced by
|
||||
// useRef plugin to concat into one.
|
||||
gulp
|
||||
.src('src/lib/codemirror/lib/codemirror.css')
|
||||
.pipe(gulp.dest('build/lib/codemirror/lib')),
|
||||
.pipe(gulp.dest(`build/lib/codemirror/lib`)),
|
||||
gulp
|
||||
.src('src/lib/codemirror/addon/hint/show-hint.css')
|
||||
.pipe(gulp.dest('build/lib/codemirror/addon/hint')),
|
||||
.pipe(gulp.dest(`build/lib/codemirror/addon/hint`)),
|
||||
gulp
|
||||
.src('src/lib/codemirror/addon/fold/foldgutter.css')
|
||||
.pipe(gulp.dest('build/lib/codemirror/addon/fold')),
|
||||
.pipe(gulp.dest(`build/lib/codemirror/addon/fold`)),
|
||||
gulp
|
||||
.src('src/lib/codemirror/addon/dialog/dialog.css')
|
||||
.pipe(gulp.dest('build/lib/codemirror/addon/dialog')),
|
||||
.pipe(gulp.dest(`build/lib/codemirror/addon/dialog`)),
|
||||
gulp.src('src/lib/hint.min.css').pipe(gulp.dest('build/lib')),
|
||||
gulp.src('src/lib/inlet.css').pipe(gulp.dest('build/lib')),
|
||||
// gulp.src('src/style.css').pipe(gulp.dest('build')),
|
||||
@@ -96,34 +104,37 @@ gulp.task('copyFiles', function () {
|
||||
'src/Inconsolata.ttf',
|
||||
'src/Monoid.ttf'
|
||||
])
|
||||
.pipe(gulp.dest('app'))
|
||||
.pipe(gulp.dest(APP_FOLDER))
|
||||
);
|
||||
});
|
||||
|
||||
gulp.task('useRef', function () {
|
||||
return gulp.src('build/index.html').pipe(useref()).pipe(gulp.dest('app'));
|
||||
return gulp
|
||||
.src('build/index.html')
|
||||
.pipe(useref())
|
||||
.pipe(gulp.dest(APP_FOLDER));
|
||||
});
|
||||
|
||||
gulp.task('concatSwRegistration', function () {
|
||||
const bundleFile = fs
|
||||
.readdirSync('app')
|
||||
.readdirSync(APP_FOLDER)
|
||||
.filter(allFilesPaths => allFilesPaths.match(/bundle.*\.js$/) !== null)[0];
|
||||
|
||||
console.log('matched', bundleFile);
|
||||
|
||||
return gulp
|
||||
.src(['src/service-worker-registration.js', `app/${bundleFile}`])
|
||||
.src(['src/service-worker-registration.js', `${APP_FOLDER}/${bundleFile}`])
|
||||
.pipe(concat(bundleFile))
|
||||
.pipe(gulp.dest('app'));
|
||||
.pipe(gulp.dest(APP_FOLDER));
|
||||
});
|
||||
|
||||
gulp.task('minify', function () {
|
||||
// minifyJs('app/script.js');
|
||||
// minifyJs('app/vendor.js');
|
||||
minifyJs('app/lib/screenlog.js');
|
||||
minifyJs(`${APP_FOLDER}/lib/screenlog.js`);
|
||||
|
||||
return gulp
|
||||
.src('app/*.css')
|
||||
.src(`${APP_FOLDER}/*.css`)
|
||||
.pipe(
|
||||
cleanCSS(
|
||||
{
|
||||
@@ -136,7 +147,7 @@ gulp.task('minify', function () {
|
||||
}
|
||||
)
|
||||
)
|
||||
.pipe(gulp.dest('app'));
|
||||
.pipe(gulp.dest(APP_FOLDER));
|
||||
});
|
||||
|
||||
gulp.task('fixIndex', function (cb) {
|
||||
@@ -159,7 +170,7 @@ gulp.task('fixIndex', function (cb) {
|
||||
|
||||
gulp.task('generate-service-worker', function (callback) {
|
||||
var swPrecache = require('sw-precache');
|
||||
var rootDir = 'app';
|
||||
var rootDir = APP_FOLDER;
|
||||
|
||||
swPrecache.write(
|
||||
`${rootDir}/service-worker.js`,
|
||||
@@ -178,7 +189,7 @@ gulp.task('generate-service-worker', function (callback) {
|
||||
|
||||
gulp.task('packageExtension', function () {
|
||||
child_process.execSync('rm -rf extension');
|
||||
child_process.execSync('cp -R app extension');
|
||||
child_process.execSync(`cp -R ${APP_FOLDER} extension`);
|
||||
child_process.execSync('cp src/manifest.json extension');
|
||||
child_process.execSync('cp src/options.js extension');
|
||||
child_process.execSync('cp src/options.html extension');
|
||||
@@ -210,7 +221,7 @@ gulp.task('buildWebsite', function () {
|
||||
gulp.task('buildDistFolder', function (cb) {
|
||||
child_process.execSync('rm -rf dist');
|
||||
child_process.execSync('mv packages/website/_site dist');
|
||||
child_process.execSync('mv app dist/');
|
||||
child_process.execSync(`mv ${APP_FOLDER} dist/`);
|
||||
cb();
|
||||
});
|
||||
|
||||
|
17
netlify.toml
17
netlify.toml
@@ -18,10 +18,19 @@ ID = "webmaker"
|
||||
# been specified, include it in the publish directory path.
|
||||
publish = "dist"
|
||||
|
||||
# The following redirect is intended for use with most SPAs that handle
|
||||
# routing internally.
|
||||
[[redirects]]
|
||||
from = "https://preview.webmaker.app/*"
|
||||
to = "/app/preview/:splat"
|
||||
to = "/create/preview/:splat"
|
||||
status = 200
|
||||
force = true
|
||||
force = true
|
||||
|
||||
[[redirects]]
|
||||
from = "https://preview.v6--webmaker.netlify.app/*"
|
||||
to = "/create/preview/:splat"
|
||||
status = 200
|
||||
force = true
|
||||
|
||||
[[redirects]]
|
||||
from = "/create/*"
|
||||
to = "/create/index.html"
|
||||
status = 200
|
21275
package-lock.json
generated
21275
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
17
package.json
17
package.json
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "web-maker",
|
||||
"version": "5.3.0",
|
||||
"version": "6.0.0",
|
||||
"description": "A blazing fast & offline web playground",
|
||||
"scripts": {
|
||||
"start": "concurrently --kill-others \"gulp start-preview-server\" \"npm run -s dev\"",
|
||||
"build": "preact build --template src/index.html --prerender false --no-inline-css --sw false --esm false",
|
||||
"dev": "preact watch --template src/index.html",
|
||||
"build": "preact build --template src/index.ejs --prerender false --no-inline-css --sw false",
|
||||
"dev": "preact watch --template src/index.ejs",
|
||||
"serve-website": "cd packages/website; npm start",
|
||||
"build-website": "cd packages/website; npm run build",
|
||||
"lint": "eslint src",
|
||||
@@ -65,24 +65,25 @@
|
||||
"markdown-it": "^8.4.2",
|
||||
"markdown-it-anchor": "^5.0.2",
|
||||
"merge-stream": "^1.0.1",
|
||||
"preact-cli": "^3.0.0",
|
||||
"preact-cli": "^4.0.0-next.6",
|
||||
"sw-precache": "^5.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emmetio/codemirror-plugin": "^0.5.4",
|
||||
"@lingui/react": "^2.8.3",
|
||||
"canvas-confetti": "^1.9.2",
|
||||
"code-blast-codemirror": "chinchang/code-blast-codemirror#web-maker",
|
||||
"codemirror": "^5.37.0",
|
||||
"codemirror": "^5.65.16",
|
||||
"copy-webpack-plugin": "^4.5.1",
|
||||
"esprima": "^4.0.0",
|
||||
"firebase": "^8.10.0",
|
||||
"jszip": "^3.1.5",
|
||||
"preact": "^10.5.13",
|
||||
"preact": "^10.17.0",
|
||||
"preact-portal": "^1.1.3",
|
||||
"preact-render-to-string": "^5.1.4",
|
||||
"preact-router": "^3.2.1",
|
||||
"prettier": "^2.2.1",
|
||||
"react-inspector": "^2.3.0",
|
||||
"prettier": "^3.0.2",
|
||||
"react-inspector": "^6.0.2",
|
||||
"split.js": "^1.5.11"
|
||||
},
|
||||
"engines": {
|
||||
|
@@ -7,8 +7,11 @@ excludeFromSitemap: true
|
||||
|
||||
<div class="ta-c">
|
||||
<figure>
|
||||
<img src="/images/404.png" style="width:65vh" />
|
||||
<figcaption>Image by https://icons8.com</figcaption>
|
||||
<img src="/images/404.png" style="width: 65vh" />
|
||||
<figcaption style="opacity: 0.5">Image by https://icons8.com</figcaption>
|
||||
</figure>
|
||||
<p>Uh oh, the page you wanted to see isn't here. How about <a href="/">going to the homepage</a>?</p>
|
||||
<p style="margin-top: 2rem">
|
||||
Uh oh, the page you wanted to see isn't here. How about
|
||||
<a href="/">going to the homepage</a>?
|
||||
</p>
|
||||
</div>
|
||||
|
@@ -114,7 +114,7 @@
|
||||
padding: 1rem;
|
||||
max-width: var(--layout-max-width);
|
||||
margin: 0 auto;
|
||||
min-height: 55vh;
|
||||
min-height: calc(100dvh - 4rem);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 700px) {
|
||||
@@ -514,7 +514,7 @@
|
||||
A blazing fast & offline frontend playground in your browser
|
||||
</h2>
|
||||
<div style="margin-top: 30px" id="cta" class="mb-2">
|
||||
<a class="btn download-btn web-app-btn" href="/app/">
|
||||
<a class="btn download-btn web-app-btn" href="/create/">
|
||||
<span>Open Web App</span>
|
||||
</a>
|
||||
<p style="margin-top: 3px">
|
||||
|
@@ -1,5 +1,3 @@
|
||||
// var CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');
|
||||
|
||||
/**
|
||||
* Function that mutates original webpack config.
|
||||
* Supports asynchronous changes when promise is returned.
|
||||
@@ -21,9 +19,9 @@ export default function (config, env, helpers) {
|
||||
htmlWebpackPlugin.plugin.options.favicon = false;
|
||||
|
||||
// Required for lingui-macros
|
||||
let { rule } = helpers.getLoadersByName(config, 'babel-loader')[0];
|
||||
let babelConfig = rule.options;
|
||||
babelConfig.plugins.push('macros');
|
||||
// let { rule } = helpers.getLoadersByName(config, 'babel-loader')[0];
|
||||
// let babelConfig = rule.options;
|
||||
// babelConfig.plugins.push('macros');
|
||||
|
||||
if (env.isProd) {
|
||||
config.devtool = false; // disable sourcemaps
|
||||
@@ -33,25 +31,5 @@ export default function (config, env, helpers) {
|
||||
|
||||
// Remove the default hash append in chunk name
|
||||
config.output.chunkFilename = '[name].chunk.js';
|
||||
|
||||
// config.plugins.push(
|
||||
// new CommonsChunkPlugin({
|
||||
// name: 'vendor',
|
||||
// minChunks: ({ resource }) => /node_modules/.test(resource)
|
||||
// })
|
||||
// );
|
||||
|
||||
const swPlugin = helpers.getPluginsByName(
|
||||
config,
|
||||
'SWPrecacheWebpackPlugin'
|
||||
)[0];
|
||||
if (swPlugin) {
|
||||
// config.plugins.splice(swPlugin.index, 1);
|
||||
}
|
||||
|
||||
const uglifyPlugin = helpers.getPluginsByName(config, 'UglifyJsPlugin')[0];
|
||||
if (uglifyPlugin) {
|
||||
// config.plugins.splice(uglifyPlugin.index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
20
preview/detached-window.js
Normal file
20
preview/detached-window.js
Normal file
@@ -0,0 +1,20 @@
|
||||
window.addEventListener('message', e => {
|
||||
// Recieving from app window
|
||||
if (e.data && e.data.contents && e.data.contents.match(/<html/)) {
|
||||
const frame = document.querySelector('iframe');
|
||||
frame.src = frame.src;
|
||||
setTimeout(() => {
|
||||
frame.contentDocument.open();
|
||||
frame.contentDocument.write(e.data.contents);
|
||||
frame.contentDocument.close();
|
||||
}, 10);
|
||||
}
|
||||
if (e.data && e.data.url && e.data.url.match(/index\.html/)) {
|
||||
document.querySelector('iframe').src = e.data.url;
|
||||
}
|
||||
|
||||
// Recieving from preview iframe
|
||||
if (e.data && e.data.logs) {
|
||||
window.opener.postMessage(e.data, '*');
|
||||
}
|
||||
});
|
22
preview/preview.html
Normal file
22
preview/preview.html
Normal file
@@ -0,0 +1,22 @@
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
#demo-frame {
|
||||
border: 0;
|
||||
width: 100%;
|
||||
background: white;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<body>
|
||||
<iframe
|
||||
src="about://blank"
|
||||
frameborder="0"
|
||||
id="demo-frame"
|
||||
allowfullscreen
|
||||
></iframe>
|
||||
|
||||
<script src="detached-window.js"></script>
|
||||
</body>
|
@@ -3,58 +3,59 @@
|
||||
import CodeMirror from 'codemirror';
|
||||
|
||||
// Make CodeMirror available globally so the modes' can register themselves.
|
||||
window.CodeMirror = CodeMirror
|
||||
window.CodeMirror = CodeMirror;
|
||||
|
||||
if (!CodeMirror.modeURL) CodeMirror.modeURL = 'lib/codemirror/mode/%N/%N.js';
|
||||
if (!CodeMirror.modeURL) CodeMirror.modeURL = '/lib/codemirror/mode/%N/%N.js';
|
||||
|
||||
var loading = {}
|
||||
var loading = {};
|
||||
|
||||
function splitCallback(cont, n) {
|
||||
var countDown = n
|
||||
var countDown = n;
|
||||
return function () {
|
||||
if (--countDown === 0) cont()
|
||||
}
|
||||
if (--countDown === 0) cont();
|
||||
};
|
||||
}
|
||||
|
||||
function ensureDeps(mode, cont) {
|
||||
var deps = CodeMirror.modes[mode].dependencies
|
||||
if (!deps) return cont()
|
||||
var missing = []
|
||||
var deps = CodeMirror.modes[mode].dependencies;
|
||||
if (!deps) return cont();
|
||||
var missing = [];
|
||||
for (var i = 0; i < deps.length; ++i) {
|
||||
if (!CodeMirror.modes.hasOwnProperty(deps[i])) missing.push(deps[i])
|
||||
if (!CodeMirror.modes.hasOwnProperty(deps[i])) missing.push(deps[i]);
|
||||
}
|
||||
if (!missing.length) return cont()
|
||||
var split = splitCallback(cont, missing.length)
|
||||
for (i = 0; i < missing.length; ++i) CodeMirror.requireMode(missing[i], split)
|
||||
if (!missing.length) return cont();
|
||||
var split = splitCallback(cont, missing.length);
|
||||
for (i = 0; i < missing.length; ++i)
|
||||
CodeMirror.requireMode(missing[i], split);
|
||||
}
|
||||
|
||||
CodeMirror.requireMode = function (mode, cont) {
|
||||
if (typeof mode !== 'string') mode = mode.name
|
||||
if (CodeMirror.modes.hasOwnProperty(mode)) return ensureDeps(mode, cont)
|
||||
if (loading.hasOwnProperty(mode)) return loading[mode].push(cont)
|
||||
if (typeof mode !== 'string') mode = mode.name;
|
||||
if (CodeMirror.modes.hasOwnProperty(mode)) return ensureDeps(mode, cont);
|
||||
if (loading.hasOwnProperty(mode)) return loading[mode].push(cont);
|
||||
|
||||
var file = CodeMirror.modeURL.replace(/%N/g, mode)
|
||||
var file = CodeMirror.modeURL.replace(/%N/g, mode);
|
||||
|
||||
var script = document.createElement('script')
|
||||
script.src = file
|
||||
var others = document.getElementsByTagName('script')[0]
|
||||
var list = loading[mode] = [cont]
|
||||
var script = document.createElement('script');
|
||||
script.src = file;
|
||||
var others = document.getElementsByTagName('script')[0];
|
||||
var list = (loading[mode] = [cont]);
|
||||
|
||||
CodeMirror.on(script, 'load', function () {
|
||||
ensureDeps(mode, function () {
|
||||
for (var i = 0; i < list.length; ++i) list[i]()
|
||||
})
|
||||
})
|
||||
for (var i = 0; i < list.length; ++i) list[i]();
|
||||
});
|
||||
});
|
||||
|
||||
others.parentNode.insertBefore(script, others)
|
||||
}
|
||||
others.parentNode.insertBefore(script, others);
|
||||
};
|
||||
|
||||
CodeMirror.autoLoadMode = function (instance, mode) {
|
||||
if (CodeMirror.modes.hasOwnProperty(mode)) return
|
||||
if (CodeMirror.modes.hasOwnProperty(mode)) return;
|
||||
|
||||
CodeMirror.requireMode(mode, function () {
|
||||
instance.setOption('mode', instance.getOption('mode'))
|
||||
})
|
||||
}
|
||||
instance.setOption('mode', instance.getOption('mode'));
|
||||
});
|
||||
};
|
||||
|
||||
export default CodeMirror
|
||||
export default CodeMirror;
|
||||
|
BIN
src/assets/pro-panda-flying.png
Normal file
BIN
src/assets/pro-panda-flying.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 216 KiB |
BIN
src/assets/pro-panda.png
Normal file
BIN
src/assets/pro-panda.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 201 KiB |
357
src/components/Assets.jsx
Normal file
357
src/components/Assets.jsx
Normal file
@@ -0,0 +1,357 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import firebase from 'firebase/app';
|
||||
import 'firebase/storage';
|
||||
import { HStack, Stack, VStack } from './Stack';
|
||||
import { copyToClipboard } from '../utils';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { ProBadge } from './ProBadge';
|
||||
import { LoaderWithText } from './Loader';
|
||||
import { Text } from './Text';
|
||||
import { Icon } from './Icons';
|
||||
|
||||
function getFileType(url) {
|
||||
// get extension from a url using URL API
|
||||
const ext = new URL(url).pathname.split('.').pop();
|
||||
if (['jpg', 'jpeg', 'png', 'gif', 'svg'].includes(ext)) {
|
||||
return 'image';
|
||||
}
|
||||
return ext;
|
||||
}
|
||||
const Assets = ({ onProBtnClick, onLoginBtnClick }) => {
|
||||
const [files, setFiles] = useState([]);
|
||||
const [isFetchingFiles, setIsFetchingFiles] = useState(false);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [filteredFiles, setFilteredFiles] = useState([]);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const [uploadProgress, setUploadProgress] = useState();
|
||||
const [listType, setListType] = useState('grid');
|
||||
|
||||
const storageRef = firebase.storage().ref(`assets/${window.user?.uid}`);
|
||||
|
||||
const uploadFile = file => {
|
||||
if (file.size > 1024 * 1024) {
|
||||
// 1MB limit
|
||||
alert('File size must be less than 1MB');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsUploading(true);
|
||||
const metadata = {
|
||||
cacheControl: 'public, max-age=3600' // 1 hr
|
||||
};
|
||||
|
||||
const fileRef = storageRef.child(file.name);
|
||||
const task = fileRef.put(file, metadata);
|
||||
|
||||
task.on(
|
||||
'state_changed',
|
||||
snapshot => {
|
||||
// Observe state change events such as progress, pause, and resume
|
||||
// Get task progress, including the number of bytes uploaded and the total number of bytes to be uploaded
|
||||
var progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
|
||||
console.log('Upload is ' + progress + '% done');
|
||||
},
|
||||
error => {
|
||||
// Handle unsuccessful uploads
|
||||
setIsUploading(false);
|
||||
console.error('File upload error:', error);
|
||||
alertsService.add('⚠️ File upload failed');
|
||||
},
|
||||
() => {
|
||||
// uploadTask.snapshot.ref.getDownloadURL().then((downloadURL) => {
|
||||
// console.log('File available at', downloadURL);
|
||||
// });
|
||||
|
||||
alertsService.add('File uploaded successfully');
|
||||
|
||||
fetchFiles();
|
||||
setIsUploading(false);
|
||||
}
|
||||
);
|
||||
};
|
||||
// Function to handle file upload
|
||||
const handleFileUpload = e => {
|
||||
const file = e.target.files[0];
|
||||
uploadFile(file);
|
||||
};
|
||||
|
||||
// Function to fetch existing files
|
||||
const fetchFiles = () => {
|
||||
setIsFetchingFiles(true);
|
||||
storageRef
|
||||
.listAll()
|
||||
.then(result => {
|
||||
const filePromises = result.items.map(item => {
|
||||
return item.getDownloadURL().then(url => {
|
||||
return { name: item.name, url };
|
||||
});
|
||||
});
|
||||
|
||||
Promise.all(filePromises).then(files => {
|
||||
files.forEach(f => (f.ext = getFileType(f.url)));
|
||||
setFiles(files);
|
||||
});
|
||||
setIsFetchingFiles(false);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('File fetch error:', error);
|
||||
setIsFetchingFiles(false);
|
||||
});
|
||||
};
|
||||
|
||||
// Function to handle search input change
|
||||
const handleSearchChange = e => {
|
||||
const term = e.target.value;
|
||||
setSearchTerm(term);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (window.user?.isPro) {
|
||||
fetchFiles();
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (searchTerm) {
|
||||
setFilteredFiles(
|
||||
files.filter(file =>
|
||||
file.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
)
|
||||
);
|
||||
} else {
|
||||
setFilteredFiles(files);
|
||||
}
|
||||
}, [files, searchTerm]);
|
||||
|
||||
const [isDropTarget, setIsDropTarget] = useState(false);
|
||||
const handleDragDropEvent = e => {
|
||||
if (e.type === 'dragover') {
|
||||
// required for drop to work
|
||||
e.preventDefault();
|
||||
} else if (e.type === 'dragleave') {
|
||||
e.preventDefault();
|
||||
// so that individual nested elements don't trigger dragleave
|
||||
if (e.currentTarget.contains(e.target)) return;
|
||||
setIsDropTarget(false);
|
||||
} else if (e.type === 'dragenter') {
|
||||
setIsDropTarget(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDrop = e => {
|
||||
e.preventDefault();
|
||||
setIsDropTarget(false);
|
||||
|
||||
if (e.dataTransfer.items) {
|
||||
const file = e.dataTransfer.items[0].getAsFile();
|
||||
uploadFile(file);
|
||||
}
|
||||
};
|
||||
|
||||
const [lastCopiedFile, setLastCopiedFile] = useState({ name: '', count: 0 });
|
||||
const copyFileUrl = url => {
|
||||
let copyContent = url;
|
||||
if (lastCopiedFile.name === url) {
|
||||
lastCopiedFile.count = (lastCopiedFile.count + 1) % 3;
|
||||
} else {
|
||||
lastCopiedFile.count = 0;
|
||||
lastCopiedFile.name = url;
|
||||
}
|
||||
|
||||
switch (lastCopiedFile.count) {
|
||||
case 0:
|
||||
copyContent = url;
|
||||
break;
|
||||
case 1:
|
||||
copyContent = `<img src="${url}" />`;
|
||||
break;
|
||||
case 2:
|
||||
copyContent = `url("${url}")`;
|
||||
break;
|
||||
}
|
||||
setLastCopiedFile({ ...lastCopiedFile });
|
||||
|
||||
copyToClipboard(copyContent).then(() => {
|
||||
switch (lastCopiedFile.count) {
|
||||
case 0:
|
||||
alertsService.add('File URL copied');
|
||||
break;
|
||||
case 1:
|
||||
alertsService.add('File URL copied as <IMG> tag');
|
||||
break;
|
||||
case 2:
|
||||
alertsService.add('File URL copied as CSS image URL');
|
||||
break;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const removeFileHandler = index => {
|
||||
const file = files[index];
|
||||
const answer = confirm(`Are you sure you want to delete "${file.name}"?`);
|
||||
if (!answer) return;
|
||||
const fileRef = storageRef.child(file.name);
|
||||
fileRef
|
||||
.delete()
|
||||
.then(() => {
|
||||
alertsService.add('File deleted successfully');
|
||||
setFiles(files.filter((_, i) => i !== index));
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('File delete error:', error);
|
||||
});
|
||||
};
|
||||
|
||||
if (!window.user?.isPro) {
|
||||
return (
|
||||
<VStack align="stretch" gap={2}>
|
||||
<p>Assets feature is available in PRO plan.</p>
|
||||
<button
|
||||
class="btn btn--pro"
|
||||
onClick={window.user ? onProBtnClick : onLoginBtnClick}
|
||||
>
|
||||
<HStack gap={1} fullWidth justify="center">
|
||||
{window.user ? <>Upgrade to PRO</> : <>Login & upgrade to PRO</>}
|
||||
</HStack>
|
||||
</button>
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div
|
||||
onDragEnter={handleDragDropEvent}
|
||||
onDragLeave={handleDragDropEvent}
|
||||
onDragOver={handleDragDropEvent}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
<HStack gap={1} align="center">
|
||||
<h1>
|
||||
<Trans>Assets</Trans>
|
||||
</h1>
|
||||
<ProBadge />
|
||||
</HStack>
|
||||
|
||||
<div
|
||||
class="asset-manager__upload-box"
|
||||
style={{
|
||||
background: isDropTarget ? '#19a61940' : 'transparent',
|
||||
borderColor: isDropTarget ? 'limegreen' : null
|
||||
}}
|
||||
>
|
||||
{isUploading ? <div class="asset-manager__progress-bar"></div> : null}
|
||||
|
||||
<div style={{ visibility: isUploading ? 'hidden' : 'visible' }}>
|
||||
<VStack gap={1} align="stretch">
|
||||
<label style="background: #00000001">
|
||||
<Text tag="p" align="center">
|
||||
Drop files or click here to upload
|
||||
</Text>
|
||||
<Text tag="p" appearance="secondary" align="center">
|
||||
File should be max 300KB in size
|
||||
</Text>
|
||||
<input
|
||||
type="file"
|
||||
onChange={handleFileUpload}
|
||||
style={{ marginTop: 'auto', display: 'none' }}
|
||||
/>
|
||||
</label>
|
||||
</VStack>
|
||||
</div>
|
||||
</div>
|
||||
{isFetchingFiles && <LoaderWithText>Fetching files...</LoaderWithText>}
|
||||
<VStack align="stretch" gap={1}>
|
||||
{files.length ? (
|
||||
<Stack gap={1}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search files"
|
||||
value={searchTerm}
|
||||
onChange={handleSearchChange}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
<button
|
||||
class={`btn btn--dark ${
|
||||
listType === 'list' ? 'btn--active' : ''
|
||||
} hint--rounded hint--top-left`}
|
||||
onClick={() => setListType('list')}
|
||||
aria-label="List view"
|
||||
>
|
||||
<Icon name="view-list" />
|
||||
</button>
|
||||
<button
|
||||
class={`btn btn--dark ${
|
||||
listType === 'grid' ? 'btn--active' : ''
|
||||
} hint--rounded hint--top-left`}
|
||||
onClick={() => setListType('grid')}
|
||||
aria-label="Grid view"
|
||||
>
|
||||
<Icon name="view-grid" />
|
||||
</button>
|
||||
</Stack>
|
||||
) : null}
|
||||
<div
|
||||
class={`asset-manager__file-container ${
|
||||
listType === 'grid' ? 'asset-manager__file-container--grid' : ''
|
||||
}`}
|
||||
>
|
||||
{filteredFiles.map((file, index) => (
|
||||
<div
|
||||
key={index}
|
||||
class={`asset-manager__file ${
|
||||
listType === 'grid' ? 'asset-manager__file--grid' : ''
|
||||
}`}
|
||||
>
|
||||
{/* <a href={file.url} target="_blank" rel="noopener noreferrer"> */}
|
||||
{file.ext === 'image' ? (
|
||||
<img src={file.url} class="asset-manager__file-image" />
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
display: 'flex'
|
||||
}}
|
||||
class="asset-manager__file-image"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="#ffffff33"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path d="M13,9V3.5L18.5,9M6,2C4.89,2 4,2.89 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2H6Z" />
|
||||
</svg>
|
||||
<span className="asset-manager__file-ext">{file.ext}</span>
|
||||
</div>
|
||||
)}
|
||||
<div class="asset-manager__file-actions">
|
||||
<Stack gap={1} fullWidth justify="center">
|
||||
<button
|
||||
class={`btn btn--dark ${
|
||||
listType === 'list' ? 'btn--active' : ''
|
||||
} hint--rounded hint--top hint--medium`}
|
||||
onClick={() => copyFileUrl(file.url)}
|
||||
aria-label="Copy URL (or keep clicking to copy other formats)"
|
||||
>
|
||||
<Icon name="copy" />
|
||||
</button>
|
||||
<button
|
||||
class={`btn btn--dark ${
|
||||
listType === 'list' ? 'btn--active' : ''
|
||||
} hint--rounded hint--top-left`}
|
||||
onClick={() => removeFileHandler(index)}
|
||||
aria-label="Delete"
|
||||
>
|
||||
<Icon name="trash" />
|
||||
</button>
|
||||
</Stack>
|
||||
</div>
|
||||
<span class="asset-manager__file-name">{file.name}</span>
|
||||
{/* </a> */}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</VStack>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { Assets };
|
@@ -44,18 +44,18 @@ window.MonacoEnvironment = {
|
||||
getWorkerUrl(moduleId, label) {
|
||||
switch (label) {
|
||||
case 'html':
|
||||
return 'lib/monaco/workers/html.worker.bundle.js';
|
||||
return '/lib/monaco/workers/html.worker.bundle.js';
|
||||
case 'json':
|
||||
return 'lib/monaco/workers/json.worker.bundle.js';
|
||||
return '/lib/monaco/workers/json.worker.bundle.js';
|
||||
case 'css':
|
||||
case 'scss':
|
||||
case 'less':
|
||||
return 'lib/monaco/workers/css.worker.bundle.js';
|
||||
return '/lib/monaco/workers/css.worker.bundle.js';
|
||||
case 'typescript':
|
||||
case 'javascript':
|
||||
return 'lib/monaco/workers/ts.worker.bundle.js';
|
||||
return '/lib/monaco/workers/ts.worker.bundle.js';
|
||||
default:
|
||||
return 'lib/monaco/workers/editor.worker.bundle.js';
|
||||
return '/lib/monaco/workers/editor.worker.bundle.js';
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -235,9 +235,9 @@ export default class CodeEditor extends Component {
|
||||
if (this.props.type === 'monaco') {
|
||||
if (!monacoDepsDeferred) {
|
||||
monacoDepsDeferred = deferred();
|
||||
loadCss({ url: 'lib/monaco/monaco.css', id: 'monaco-css' });
|
||||
loadCss({ url: '/lib/monaco/monaco.css', id: 'monaco-css' });
|
||||
import(
|
||||
/* webpackChunkName: "monaco" */ '../lib/monaco/monaco.bundle.js'
|
||||
/* webpackChunkName: "monaco" */ '/lib/monaco/monaco.bundle.js'
|
||||
).then(() => {
|
||||
monacoDepsDeferred.resolve();
|
||||
});
|
||||
|
@@ -12,15 +12,12 @@ class LogRow extends Component {
|
||||
const theme = {
|
||||
...chromeDark,
|
||||
...{
|
||||
OBJECT_VALUE_STRING_COLOR: 'green',
|
||||
BASE_FONT_SIZE: '20px',
|
||||
TREENODE_FONT_SIZE: '20px'
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Inspector theme={theme} theme="chromeDark" data={this.props.data} />
|
||||
);
|
||||
return <Inspector theme={theme} data={this.props.data} />;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -21,6 +21,12 @@ const minCodeWrapSize = 33;
|
||||
/* global htmlCodeEl
|
||||
*/
|
||||
|
||||
const PREVIEW_FRAME_HOST = window.DEBUG
|
||||
? 'http://localhost:7888'
|
||||
: `https://wbmakr.com`;
|
||||
|
||||
let cachedSandboxAttribute = '';
|
||||
|
||||
export default class ContentWrap extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@@ -160,22 +166,44 @@ export default class ContentWrap extends Component {
|
||||
log('✉️ Sending message to detached window');
|
||||
this.detachedWindow.postMessage({ contents }, '*');
|
||||
} else {
|
||||
const writeInsideIframe = () => {
|
||||
this.frame.contentDocument.open();
|
||||
this.frame.contentDocument.write(contents);
|
||||
this.frame.contentDocument.close();
|
||||
// 1. we refresh the frame so that all JS is cleared in the frame. this will
|
||||
// break the iframe since sandboxed frame isn't served by SW (needed for offline support)
|
||||
// 2. we cache and remove the sandbox attribute and refresh again so that it gets served by SW
|
||||
// 3. we add back cached sandbox attr & write the contents to the iframe
|
||||
const refreshAndDo = fn => {
|
||||
Promise.race([
|
||||
// Just in case onload promise doesn't resolves
|
||||
new Promise(resolve => {
|
||||
setTimeout(resolve, 200);
|
||||
}),
|
||||
new Promise(resolve => {
|
||||
this.frame.onload = resolve;
|
||||
})
|
||||
]).then(fn);
|
||||
// Setting to blank string cause frame to reload
|
||||
this.frame.src = this.frame.src;
|
||||
};
|
||||
Promise.race([
|
||||
// Just in case onload promise doesn't resolves
|
||||
new Promise(resolve => {
|
||||
setTimeout(resolve, 200);
|
||||
}),
|
||||
new Promise(resolve => {
|
||||
this.frame.onload = resolve;
|
||||
})
|
||||
]).then(writeInsideIframe);
|
||||
// Setting to blank string cause frame to reload
|
||||
this.frame.src = '';
|
||||
const writeInsideIframe = () => {
|
||||
if (!cachedSandboxAttribute && window.DEBUG) {
|
||||
alert('sandbox empty');
|
||||
}
|
||||
// console.log('setting back sandbox attr', sandbox);
|
||||
this.frame.setAttribute('sandbox', cachedSandboxAttribute);
|
||||
this.frame.removeAttribute('sweet');
|
||||
// console.log('sending postmessage');
|
||||
this.frame.contentWindow.postMessage({ contents }, '*');
|
||||
// this.frame.contentDocument.open();
|
||||
// this.frame.contentDocument.write(contents);
|
||||
// this.frame.contentDocument.close();
|
||||
};
|
||||
refreshAndDo(() => {
|
||||
cachedSandboxAttribute = this.frame.getAttribute('sandbox');
|
||||
// console.log('removing sandbox', sandbox);
|
||||
// this.frame.setAttribute('sweet', sandbox);
|
||||
this.frame.removeAttribute('sandbox');
|
||||
refreshAndDo(writeInsideIframe);
|
||||
});
|
||||
// refreshAndDo(writeInsideIframe);
|
||||
}
|
||||
} else {
|
||||
// we need to store user script in external JS file to prevent inline-script
|
||||
@@ -233,7 +261,7 @@ export default class ContentWrap extends Component {
|
||||
};
|
||||
log('🔎 setPreviewContent', isForced);
|
||||
const targetFrame = this.detachedWindow
|
||||
? this.detachedWindow.document.querySelector('iframe')
|
||||
? this.detachedWindow //this.detachedWindow.document.querySelector('iframe')
|
||||
: this.frame;
|
||||
|
||||
const cssMode = this.props.currentItem.cssMode;
|
||||
@@ -242,7 +270,8 @@ export default class ContentWrap extends Component {
|
||||
if (
|
||||
!isForced &&
|
||||
currentCode.html === this.codeInPreview.html &&
|
||||
currentCode.js === this.codeInPreview.js
|
||||
currentCode.js === this.codeInPreview.js &&
|
||||
false
|
||||
) {
|
||||
computeCss(
|
||||
cssMode === CssModes.ACSS ? currentCode.html : currentCode.css,
|
||||
@@ -341,7 +370,7 @@ export default class ContentWrap extends Component {
|
||||
|
||||
// Replace correct css file in LINK tags's href
|
||||
if (prefs.editorTheme) {
|
||||
window.editorThemeLinkTag.href = `lib/codemirror/theme/${prefs.editorTheme}.css`;
|
||||
window.editorThemeLinkTag.href = `./lib/codemirror/theme/${prefs.editorTheme}.css`;
|
||||
}
|
||||
|
||||
window.fontStyleTag.textContent =
|
||||
@@ -533,7 +562,7 @@ export default class ContentWrap extends Component {
|
||||
document.body.classList.add('is-detached-mode');
|
||||
|
||||
this.detachedWindow = window.open(
|
||||
'./preview.html',
|
||||
`${PREVIEW_FRAME_HOST}/preview.html`,
|
||||
'Web Maker',
|
||||
`width=${iframeWidth},height=${iframeHeight},resizable,scrollbars=yes,status=1`
|
||||
);
|
||||
@@ -901,10 +930,14 @@ export default class ContentWrap extends Component {
|
||||
</SplitPane>
|
||||
<div class="demo-side" id="js-demo-side" style="">
|
||||
<iframe
|
||||
src={`./indexpm.html`}
|
||||
ref={el => (this.frame = el)}
|
||||
frameborder="0"
|
||||
id="demo-frame"
|
||||
allowfullscreen
|
||||
sandbox="allow-downloads allow-forms allow-modals allow-pointer-lock allow-popups allow-presentation allow-scripts allow-top-navigation-by-user-activation"
|
||||
allow="accelerometer; camera; encrypted-media; display-capture; geolocation; gyroscope; microphone; midi; clipboard-read; clipboard-write; web-share"
|
||||
allowpaymentrequest="true"
|
||||
allowfullscreen="true"
|
||||
/>
|
||||
|
||||
<PreviewDimension ref={comp => (this.previewDimension = comp)} />
|
||||
|
@@ -29,7 +29,7 @@ import { FileIcon } from './FileIcon';
|
||||
const minCodeWrapSize = 33;
|
||||
const PREVIEW_FRAME_HOST = window.DEBUG
|
||||
? 'http://localhost:7888'
|
||||
: `https://preview.${location.host}`;
|
||||
: `https://wbmakr.com`;
|
||||
|
||||
/* global htmlCodeEl
|
||||
*/
|
||||
@@ -54,12 +54,10 @@ export default class ContentWrapFiles extends Component {
|
||||
// `clearConsole` is on window because it gets called from inside iframe also.
|
||||
window.clearConsole = this.clearConsole.bind(this);
|
||||
|
||||
this.consoleHeaderDblClickHandler = this.consoleHeaderDblClickHandler.bind(
|
||||
this
|
||||
);
|
||||
this.clearConsoleBtnClickHandler = this.clearConsoleBtnClickHandler.bind(
|
||||
this
|
||||
);
|
||||
this.consoleHeaderDblClickHandler =
|
||||
this.consoleHeaderDblClickHandler.bind(this);
|
||||
this.clearConsoleBtnClickHandler =
|
||||
this.clearConsoleBtnClickHandler.bind(this);
|
||||
this.toggleConsole = this.toggleConsole.bind(this);
|
||||
this.evalConsoleExpr = this.evalConsoleExpr.bind(this);
|
||||
}
|
||||
@@ -259,7 +257,7 @@ export default class ContentWrapFiles extends Component {
|
||||
obj[file.path] =
|
||||
'<script src="' +
|
||||
(chrome.extension
|
||||
? chrome.extension.getURL('lib/screenlog.js')
|
||||
? chrome.extension.getURL('/lib/screenlog.js')
|
||||
: `${location.origin}${
|
||||
window.DEBUG ? '' : BASE_PATH
|
||||
}/lib/screenlog.js`) +
|
||||
@@ -357,15 +355,16 @@ export default class ContentWrapFiles extends Component {
|
||||
|
||||
// Replace correct css file in LINK tags's href
|
||||
if (prefs.editorTheme) {
|
||||
window.editorThemeLinkTag.href = `lib/codemirror/theme/${prefs.editorTheme}.css`;
|
||||
window.editorThemeLinkTag.href = `./lib/codemirror/theme/${prefs.editorTheme}.css`;
|
||||
}
|
||||
|
||||
window.fontStyleTag.textContent = window.fontStyleTemplate.textContent.replace(
|
||||
/fontname/g,
|
||||
(prefs.editorFont === 'other'
|
||||
? prefs.editorCustomFont
|
||||
: prefs.editorFont) || 'FiraCode'
|
||||
);
|
||||
window.fontStyleTag.textContent =
|
||||
window.fontStyleTemplate.textContent.replace(
|
||||
/fontname/g,
|
||||
(prefs.editorFont === 'other'
|
||||
? prefs.editorCustomFont
|
||||
: prefs.editorFont) || 'FiraCode'
|
||||
);
|
||||
}
|
||||
|
||||
// Check all the code wrap if they are minimized or maximized
|
||||
@@ -376,7 +375,7 @@ export default class ContentWrapFiles extends Component {
|
||||
const { currentLayoutMode } = this.props;
|
||||
const prop =
|
||||
currentLayoutMode === 2 || currentLayoutMode === 5 ? 'width' : 'height';
|
||||
[htmlCodeEl].forEach(function(el) {
|
||||
[htmlCodeEl].forEach(function (el) {
|
||||
const bounds = el.getBoundingClientRect();
|
||||
const size = bounds[prop];
|
||||
if (size < 100) {
|
||||
|
@@ -5,6 +5,7 @@ import templates from '../templateList';
|
||||
import { BetaTag } from './common';
|
||||
import { trackEvent } from '../analytics';
|
||||
import Tabs, { TabPanel } from './Tabs';
|
||||
import { ProBadge } from './ProBadge';
|
||||
|
||||
export class CreateNewModal extends Component {
|
||||
constructor(props) {
|
||||
@@ -150,11 +151,10 @@ export class CreateNewModal extends Component {
|
||||
<h1 class="mt-0">Create New</h1>
|
||||
<Tabs horizontal onChange={this.modeChangeHandler}>
|
||||
<TabPanel label={option1}>
|
||||
<div class="d-f fxw-w">
|
||||
<div class="templates-container">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn--primary"
|
||||
style="margin:20px 10px"
|
||||
onClick={() => {
|
||||
trackEvent('ui', 'startBlankBtnClick');
|
||||
onBlankTemplateSelect();
|
||||
@@ -170,17 +170,17 @@ export class CreateNewModal extends Component {
|
||||
item={template}
|
||||
focusable
|
||||
onClick={onTemplateSelect.bind(null, template, false)}
|
||||
hasOptions={false}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</TabPanel>
|
||||
<TabPanel label={option2}>
|
||||
<div class="d-f fxw-w show-when-app">
|
||||
<div class="templates-container show-when-app">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn--primary"
|
||||
style="margin:20px 10px"
|
||||
onClick={() => {
|
||||
trackEvent('ui', 'startBlankFileBtnClick');
|
||||
onBlankFileTemplateSelect();
|
||||
@@ -196,14 +196,19 @@ export class CreateNewModal extends Component {
|
||||
item={template}
|
||||
focusable
|
||||
onClick={onTemplateSelect.bind(null, template, true)}
|
||||
hasOptions={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
<p>
|
||||
2 files mode creations available in Free plan. To create unlimited
|
||||
files mode creations, upgrade to <ProBadge />.
|
||||
</p>
|
||||
<div class="show-when-extension">
|
||||
Files modes is currently only available in Web app.{' '}
|
||||
<a href="https://webmaker.app/app/">Try the Web app now</a>.
|
||||
<a href="https://webmaker.app/create/">Try the Web app now</a>.
|
||||
</div>
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
@@ -1,337 +1,342 @@
|
||||
import { h, Component } from 'preact';
|
||||
import { Button } from './common';
|
||||
import { Trans, t } from '@lingui/macro';
|
||||
import { I18n } from '@lingui/react';
|
||||
import { ProBadge } from './ProBadge';
|
||||
import { HStack } from './Stack';
|
||||
import { useEffect, useState } from 'preact/hooks';
|
||||
|
||||
class JS13K extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const compoDate = new Date('August 13 2018 11:00 GMT');
|
||||
var now = new Date();
|
||||
var daysLeft;
|
||||
const JS13K = props => {
|
||||
const [daysLeft, setDaysLeft] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const compoDate = new Date('August 13 2024 11:00 GMT');
|
||||
const now = new Date();
|
||||
if (+compoDate > +now) {
|
||||
daysLeft = Math.floor((compoDate - now) / 1000 / 3600 / 24);
|
||||
const _daysLeft = Math.floor((compoDate - now) / 1000 / 3600 / 24);
|
||||
setDaysLeft(_daysLeft);
|
||||
}
|
||||
this.setState({
|
||||
daysLeft
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
render() {
|
||||
const codeSizeInKb = this.props.codeSize
|
||||
? (this.props.codeSize / 1024).toFixed(2)
|
||||
: 0;
|
||||
return (
|
||||
const codeSizeInKb = props.codeSize ? (props.codeSize / 1024).toFixed(2) : 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
role="button"
|
||||
className="flex flex-v-center"
|
||||
tabIndex="0"
|
||||
onClick={props.onClick}
|
||||
onBlur={props.onBlur}
|
||||
>
|
||||
<img src="assets/js13kgames.png" alt="JS13K Games logo" height="24" />{' '}
|
||||
<div className="footer__js13k-days-left">{daysLeft} days to go</div>
|
||||
<div
|
||||
role="button"
|
||||
class="flex flex-v-center"
|
||||
tabIndex="0"
|
||||
onClick={this.props.onClick}
|
||||
onBlur={this.props.onBlur}
|
||||
className="footer__js13k-code-size"
|
||||
style={{
|
||||
color: codeSizeInKb > 10 ? 'crimson' : 'limegreen'
|
||||
}}
|
||||
>
|
||||
<img src="assets/js13kgames.png" alt="JS13K Games logo" height="24" />{' '}
|
||||
<div class="footer__js13k-days-left">
|
||||
{this.state.daysLeft} days to go
|
||||
</div>
|
||||
<div
|
||||
class="footer__js13k-code-size"
|
||||
style={{
|
||||
color: codeSizeInKb > 10 ? 'crimson' : 'limegreen'
|
||||
}}
|
||||
>
|
||||
{codeSizeInKb} KB/ 13KB
|
||||
</div>
|
||||
<span
|
||||
class="caret"
|
||||
style={`transition:0.3s ease; transform-origin: center 2px; ${
|
||||
this.props.isOpen ? 'transform:rotate(180deg);' : ''
|
||||
}`}
|
||||
/>
|
||||
{codeSizeInKb} KB/ 13KB
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
<span
|
||||
className="caret"
|
||||
style={{
|
||||
transition: '0.3s ease',
|
||||
transformOrigin: 'center 2px',
|
||||
transform: props.isOpen ? 'rotate(180deg)' : ''
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default class Footer extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isKeyboardShortcutsModalOpen: false,
|
||||
isJs13kDropdownOpen: false
|
||||
};
|
||||
}
|
||||
layoutBtnClickhandler(layoutId) {
|
||||
this.props.layoutBtnClickHandler(layoutId);
|
||||
export const Footer = props => {
|
||||
const [isKeyboardShortcutsModalOpen, setIsKeyboardShortcutsModalOpen] =
|
||||
useState(false);
|
||||
const [isJs13kDropdownOpen, setIsJs13kDropdownOpen] = useState(false);
|
||||
|
||||
function layoutBtnClickhandler(layoutId) {
|
||||
props.layoutBtnClickHandler(layoutId);
|
||||
}
|
||||
|
||||
js13kClickHandler() {
|
||||
function js13kClickHandler() {
|
||||
// console.log(999);
|
||||
this.setState({
|
||||
isJs13kDropdownOpen: !this.state.isJs13kDropdownOpen
|
||||
});
|
||||
setIsJs13kDropdownOpen(!isJs13kDropdownOpen);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<div id="footer" class="footer">
|
||||
<div>
|
||||
<a href="/" target="_blank" rel="noopener noreferrer">
|
||||
<div class="logo" />
|
||||
</a>
|
||||
©
|
||||
<span class="web-maker-with-tag">Web Maker</span>
|
||||
<Button
|
||||
onClick={this.props.helpBtnClickHandler}
|
||||
data-event-category="ui"
|
||||
data-event-action="helpButtonClick"
|
||||
class="footer__link hint--rounded hint--top-right"
|
||||
aria-label={i18n._(t`Help`)}
|
||||
return (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<div id="footer" class="footer">
|
||||
<div>
|
||||
<a href="/" target="_blank" rel="noopener noreferrer">
|
||||
<div class="logo" />
|
||||
</a>
|
||||
©
|
||||
<span class="web-maker-with-tag">Web Maker</span>
|
||||
<Button
|
||||
onClick={props.helpBtnClickHandler}
|
||||
data-event-category="ui"
|
||||
data-event-action="helpButtonClick"
|
||||
class="footer__link hint--rounded hint--top-right"
|
||||
aria-label={i18n._(t`Help`)}
|
||||
>
|
||||
<svg
|
||||
style="width:20px; height:20px; vertical-align:text-bottom"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<svg
|
||||
style="width:20px; height:20px; vertical-align:text-bottom"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path d="M15.07,11.25L14.17,12.17C13.45,12.89 13,13.5 13,15H11V14.5C11,13.39 11.45,12.39 12.17,11.67L13.41,10.41C13.78,10.05 14,9.55 14,9C14,7.89 13.1,7 12,7A2,2 0 0,0 10,9H8A4,4 0 0,1 12,5A4,4 0 0,1 16,9C16,9.88 15.64,10.67 15.07,11.25M13,19H11V17H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12C22,6.47 17.5,2 12,2Z" />
|
||||
</svg>
|
||||
<path d="M15.07,11.25L14.17,12.17C13.45,12.89 13,13.5 13,15H11V14.5C11,13.39 11.45,12.39 12.17,11.67L13.41,10.41C13.78,10.05 14,9.55 14,9C14,7.89 13.1,7 12,7A2,2 0 0,0 10,9H8A4,4 0 0,1 12,5A4,4 0 0,1 16,9C16,9.88 15.64,10.67 15.07,11.25M13,19H11V17H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12C22,6.47 17.5,2 12,2Z" />
|
||||
</svg>
|
||||
</Button>
|
||||
<Button
|
||||
onClick={props.keyboardShortcutsBtnClickHandler}
|
||||
data-event-category="ui"
|
||||
data-event-action="keyboardShortcutButtonClick"
|
||||
class="footer__link hint--rounded hint--top-right hide-on-mobile"
|
||||
aria-label={i18n._(t`Keyboard shortcuts`)}
|
||||
>
|
||||
<svg
|
||||
style={{
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
verticalAlign: 'text-bottom'
|
||||
}}
|
||||
>
|
||||
<use xlinkHref="#keyboard-icon" />
|
||||
</svg>
|
||||
</Button>
|
||||
<a
|
||||
class="footer__link hint--rounded hint--top-right"
|
||||
aria-label={i18n._(t`Tweet about 'Web Maker'`)}
|
||||
href="http://twitter.com/share?url=https://webmaker.app/&text=Web Maker - A blazing fast %26 offline web playground! via @webmakerApp&related=webmakerApp&hashtags=web,frontend,playground,offline"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<svg
|
||||
style={{
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
verticalAlign: 'text-bottom'
|
||||
}}
|
||||
>
|
||||
<use xlinkHref="#twitter-icon" />
|
||||
</svg>
|
||||
</a>
|
||||
{props.user?.isPro ? (
|
||||
<Button
|
||||
onClick={props.proBtnClickHandler}
|
||||
data-event-category="ui"
|
||||
data-event-action="manageProFooterBtnClick"
|
||||
class="footer__link ml-1 hint--rounded hint--top-right hide-on-mobile support-link"
|
||||
aria-label={i18n._(t`Manage your PRO subscription`)}
|
||||
>
|
||||
<HStack gap={1}>
|
||||
<Trans>Manage</Trans>
|
||||
<ProBadge />
|
||||
</HStack>
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
onClick={this.props.keyboardShortcutsBtnClickHandler}
|
||||
onClick={props.proBtnClickHandler}
|
||||
data-event-category="ui"
|
||||
data-event-action="keyboardShortcutButtonClick"
|
||||
class="footer__link hint--rounded hint--top-right hide-on-mobile"
|
||||
aria-label={i18n._(t`Keyboard shortcuts`)}
|
||||
>
|
||||
<svg
|
||||
style={{
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
verticalAlign: 'text-bottom'
|
||||
}}
|
||||
>
|
||||
<use xlinkHref="#keyboard-icon" />
|
||||
</svg>
|
||||
</Button>
|
||||
<a
|
||||
class="footer__link hint--rounded hint--top-right"
|
||||
aria-label={i18n._(t`Tweet about 'Web Maker'`)}
|
||||
href="http://twitter.com/share?url=https://webmaker.app/&text=Web Maker - A blazing fast %26 offline web playground! via @webmakerApp&related=webmakerApp&hashtags=web,frontend,playground,offline"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<svg
|
||||
style={{
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
verticalAlign: 'text-bottom'
|
||||
}}
|
||||
>
|
||||
<use xlinkHref="#twitter-icon" />
|
||||
</svg>
|
||||
</a>
|
||||
<Button
|
||||
onClick={this.props.supportDeveloperBtnClickHandler}
|
||||
data-event-category="ui"
|
||||
data-event-action="supportDeveloperFooterBtnClick"
|
||||
data-event-action="proFooterBtnClick"
|
||||
class="footer__link ml-1 hint--rounded hint--top-right hide-on-mobile support-link"
|
||||
aria-label={i18n._(
|
||||
t`Support the developer by pledging some amount`
|
||||
t`Upgrade to PRO and get some advanced superpowers!`
|
||||
)}
|
||||
>
|
||||
<Trans>Donate</Trans>
|
||||
<HStack gap={1}>
|
||||
<Trans>Get</Trans>
|
||||
<ProBadge />
|
||||
</HStack>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{this.props.prefs.isJs13kModeOn ? (
|
||||
<div class="flex flex-v-center">
|
||||
<JS13K
|
||||
isOpen={this.state.isJs13kDropdownOpen}
|
||||
codeSize={this.props.codeSize}
|
||||
onClick={this.js13kClickHandler.bind(this)}
|
||||
onBlur={() =>
|
||||
setTimeout(
|
||||
() => this.setState({ isJs13kDropdownOpen: false }),
|
||||
300
|
||||
)
|
||||
}
|
||||
/>
|
||||
{this.state.isJs13kDropdownOpen && (
|
||||
<div className="js13k__dropdown">
|
||||
<button
|
||||
class="btn"
|
||||
style={{
|
||||
width: '200px',
|
||||
display: 'block',
|
||||
marginBottom: '16px'
|
||||
}}
|
||||
onClick={this.props.onJs13KDownloadBtnClick}
|
||||
>
|
||||
<Trans>Download game as zip</Trans>
|
||||
</button>
|
||||
<a
|
||||
class="btn"
|
||||
rel="noopener"
|
||||
style={{
|
||||
width: '200px',
|
||||
display: 'block',
|
||||
marginBottom: '16px'
|
||||
}}
|
||||
href="https://pasteboard.co/"
|
||||
target="_blank"
|
||||
>
|
||||
<Trans>Upload Image</Trans>
|
||||
</a>
|
||||
<button
|
||||
class="btn"
|
||||
style={{ width: '200px', display: 'block' }}
|
||||
onClick={this.props.onJs13KHelpBtnClick}
|
||||
>
|
||||
<Trans>Help</Trans>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div class="footer__right">
|
||||
<button
|
||||
onClick={this.props.saveHtmlBtnClickHandler}
|
||||
id="saveHtmlBtn"
|
||||
class="mode-btn hint--rounded hint--top-left hide-on-mobile hide-in-file-mode"
|
||||
aria-label={i18n._(t`Save as HTML file`)}
|
||||
>
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M5,20H19V18H5M19,9H15V3H9V9H5L12,16L19,9Z" />
|
||||
</svg>
|
||||
</button>
|
||||
<svg style="display: none;" xmlns="http://www.w3.org/2000/svg">
|
||||
<symbol id="codepen-logo" viewBox="0 0 120 120">
|
||||
<path
|
||||
class="outer-ring"
|
||||
d="M60.048 0C26.884 0 0 26.9 0 60.048s26.884 60 60 60.047c33.163 0 60.047-26.883 60.047-60.047 S93.211 0 60 0z M60.048 110.233c-27.673 0-50.186-22.514-50.186-50.186S32.375 9.9 60 9.9 c27.672 0 50.2 22.5 50.2 50.186S87.72 110.2 60 110.233z"
|
||||
/>
|
||||
<path
|
||||
class="inner-box"
|
||||
d="M97.147 48.319c-0.007-0.047-0.019-0.092-0.026-0.139c-0.016-0.09-0.032-0.18-0.056-0.268 c-0.014-0.053-0.033-0.104-0.05-0.154c-0.025-0.078-0.051-0.156-0.082-0.232c-0.021-0.053-0.047-0.105-0.071-0.156 c-0.033-0.072-0.068-0.143-0.108-0.211c-0.029-0.051-0.061-0.1-0.091-0.148c-0.043-0.066-0.087-0.131-0.135-0.193 c-0.035-0.047-0.072-0.094-0.109-0.139c-0.051-0.059-0.104-0.117-0.159-0.172c-0.042-0.043-0.083-0.086-0.127-0.125 c-0.059-0.053-0.119-0.104-0.181-0.152c-0.048-0.037-0.095-0.074-0.145-0.109c-0.019-0.012-0.035-0.027-0.053-0.039L61.817 23.5 c-1.072-0.715-2.468-0.715-3.54 0L24.34 46.081c-0.018 0.012-0.034 0.027-0.053 0.039c-0.05 0.035-0.097 0.072-0.144 0.1 c-0.062 0.049-0.123 0.1-0.181 0.152c-0.045 0.039-0.086 0.082-0.128 0.125c-0.056 0.055-0.108 0.113-0.158 0.2 c-0.038 0.045-0.075 0.092-0.11 0.139c-0.047 0.062-0.092 0.127-0.134 0.193c-0.032 0.049-0.062 0.098-0.092 0.1 c-0.039 0.068-0.074 0.139-0.108 0.211c-0.024 0.051-0.05 0.104-0.071 0.156c-0.031 0.076-0.057 0.154-0.082 0.2 c-0.017 0.051-0.035 0.102-0.05 0.154c-0.023 0.088-0.039 0.178-0.056 0.268c-0.008 0.047-0.02 0.092-0.025 0.1 c-0.019 0.137-0.029 0.275-0.029 0.416V71.36c0 0.1 0 0.3 0 0.418c0.006 0 0 0.1 0 0.1 c0.017 0.1 0 0.2 0.1 0.268c0.015 0.1 0 0.1 0.1 0.154c0.025 0.1 0.1 0.2 0.1 0.2 c0.021 0.1 0 0.1 0.1 0.154c0.034 0.1 0.1 0.1 0.1 0.213c0.029 0 0.1 0.1 0.1 0.1 c0.042 0.1 0.1 0.1 0.1 0.193c0.035 0 0.1 0.1 0.1 0.139c0.05 0.1 0.1 0.1 0.2 0.2 c0.042 0 0.1 0.1 0.1 0.125c0.058 0.1 0.1 0.1 0.2 0.152c0.047 0 0.1 0.1 0.1 0.1 c0.019 0 0 0 0.1 0.039L58.277 96.64c0.536 0.4 1.2 0.5 1.8 0.537c0.616 0 1.233-0.18 1.77-0.537 l33.938-22.625c0.018-0.012 0.034-0.027 0.053-0.039c0.05-0.035 0.097-0.072 0.145-0.109c0.062-0.049 0.122-0.1 0.181-0.152 c0.044-0.039 0.085-0.082 0.127-0.125c0.056-0.055 0.108-0.113 0.159-0.172c0.037-0.045 0.074-0.09 0.109-0.139 c0.048-0.062 0.092-0.127 0.135-0.193c0.03-0.049 0.062-0.098 0.091-0.146c0.04-0.07 0.075-0.141 0.108-0.213 c0.024-0.051 0.05-0.102 0.071-0.154c0.031-0.078 0.057-0.156 0.082-0.234c0.017-0.051 0.036-0.102 0.05-0.154 c0.023-0.088 0.04-0.178 0.056-0.268c0.008-0.045 0.02-0.092 0.026-0.137c0.018-0.139 0.028-0.277 0.028-0.418V48.735 C97.176 48.6 97.2 48.5 97.1 48.319z M63.238 32.073l25.001 16.666L77.072 56.21l-13.834-9.254V32.073z M56.856 32.1 v14.883L43.023 56.21l-11.168-7.471L56.856 32.073z M29.301 54.708l7.983 5.34l-7.983 5.34V54.708z M56.856 88.022L31.855 71.4 l11.168-7.469l13.833 9.252V88.022z M60.048 67.597l-11.286-7.549l11.286-7.549l11.285 7.549L60.048 67.597z M63.238 88.022V73.14 l13.834-9.252l11.167 7.469L63.238 88.022z M90.794 65.388l-7.982-5.34l7.982-5.34V65.388z"
|
||||
/>
|
||||
</symbol>
|
||||
</svg>
|
||||
|
||||
<button
|
||||
onClick={this.props.codepenBtnClickHandler}
|
||||
id="codepenBtn"
|
||||
class="mode-btn hint--rounded hint--top-left hide-on-mobile hide-in-file-mode"
|
||||
aria-label={i18n._(t`Edit on CodePen`)}
|
||||
>
|
||||
<svg>
|
||||
<use xlinkHref="#codepen-logo" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button
|
||||
id="screenshotBtn"
|
||||
class="mode-btn hint--rounded hint--top-left show-when-extension"
|
||||
onClick={this.props.screenshotBtnClickHandler}
|
||||
aria-label={i18n._(t`Take screenshot of preview`)}
|
||||
>
|
||||
<svg style="width:24px;height:24px" viewBox="0 0 24 24">
|
||||
<path d="M4,4H7L9,2H15L17,4H20A2,2 0 0,1 22,6V18A2,2 0 0,1 20,20H4A2,2 0 0,1 2,18V6A2,2 0 0,1 4,4M12,7A5,5 0 0,0 7,12A5,5 0 0,0 12,17A5,5 0 0,0 17,12A5,5 0 0,0 12,7M12,9A3,3 0 0,1 15,12A3,3 0 0,1 12,15A3,3 0 0,1 9,12A3,3 0 0,1 12,9Z" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div class="footer__separator hide-on-mobile" />
|
||||
|
||||
<button
|
||||
onClick={this.layoutBtnClickhandler.bind(this, 1)}
|
||||
id="layoutBtn1"
|
||||
class="mode-btn hide-on-mobile hide-in-file-mode"
|
||||
aria-label={i18n._(t`Switch to layout with preview on right`)}
|
||||
>
|
||||
<svg viewBox="0 0 100 100" style="transform:rotate(-90deg)">
|
||||
<use xlinkHref="#mode-icon" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={this.layoutBtnClickhandler.bind(this, 2)}
|
||||
id="layoutBtn2"
|
||||
class="mode-btn hide-on-mobile hide-in-file-mode"
|
||||
aria-label={i18n._(t`Switch to layout with preview on bottom`)}
|
||||
>
|
||||
<svg viewBox="0 0 100 100">
|
||||
<use xlinkHref="#mode-icon" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={this.layoutBtnClickhandler.bind(this, 3)}
|
||||
id="layoutBtn3"
|
||||
class="mode-btn hide-on-mobile hide-in-file-mode"
|
||||
aria-label={i18n._(t`Switch to layout with preview on left`)}
|
||||
>
|
||||
<svg viewBox="0 0 100 100" style="transform:rotate(90deg)">
|
||||
<use xlinkHref="#mode-icon" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={this.layoutBtnClickhandler.bind(this, 5)}
|
||||
id="layoutBtn5"
|
||||
class="mode-btn hide-on-mobile hide-in-file-mode"
|
||||
aria-label={i18n._(t`Switch to layout with all vertical panes`)}
|
||||
>
|
||||
<svg viewBox="0 0 100 100">
|
||||
<use xlinkHref="#vertical-mode-icon" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={this.layoutBtnClickhandler.bind(this, 4)}
|
||||
id="layoutBtn4"
|
||||
class="mode-btn hint--top-left hint--rounded hide-on-mobile"
|
||||
aria-label={i18n._(t`Toggle full screen preview`)}
|
||||
>
|
||||
<svg viewBox="0 0 100 100">
|
||||
<rect x="0" y="0" width="100" height="100" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="mode-btn hint--top-left hint--rounded hide-on-mobile"
|
||||
aria-label={i18n._(t`Detach preview`)}
|
||||
onClick={this.props.detachedPreviewBtnHandler}
|
||||
>
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M22,17V7H6V17H22M22,5A2,2 0 0,1 24,7V17C24,18.11 23.1,19 22,19H16V21H18V23H10V21H12V19H6C4.89,19 4,18.11 4,17V7A2,2 0 0,1 6,5H22M2,3V15H0V3A2,2 0 0,1 2,1H20V3H2Z" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div class="footer__separator" />
|
||||
|
||||
<button
|
||||
onClick={this.props.notificationsBtnClickHandler}
|
||||
id="notificationsBtn"
|
||||
class={`notifications-btn mode-btn hint--top-left hint--rounded ${
|
||||
this.props.hasUnseenChangelog ? 'has-new' : ''
|
||||
}`}
|
||||
aria-label={i18n._(t`See changelog`)}
|
||||
>
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M14,20A2,2 0 0,1 12,22A2,2 0 0,1 10,20H14M12,2A1,1 0 0,1 13,3V4.08C15.84,4.56 18,7.03 18,10V16L21,19H3L6,16V10C6,7.03 8.16,4.56 11,4.08V3A1,1 0 0,1 12,2Z" />
|
||||
</svg>
|
||||
<span class="notifications-btn__dot" />
|
||||
</button>
|
||||
<Button
|
||||
onClick={this.props.settingsBtnClickHandler}
|
||||
data-event-category="ui"
|
||||
data-event-action="settingsBtnClick"
|
||||
class="mode-btn hint--top-left hint--rounded"
|
||||
aria-label={i18n._(t`Settings`)}
|
||||
>
|
||||
<svg>
|
||||
<use xlinkHref="#settings-icon" />
|
||||
</svg>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</I18n>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
{props.prefs.isJs13kModeOn ? (
|
||||
<div class="flex flex-v-center">
|
||||
<JS13K
|
||||
isOpen={isJs13kDropdownOpen}
|
||||
codeSize={props.codeSize}
|
||||
onClick={js13kClickHandler}
|
||||
onBlur={() =>
|
||||
setTimeout(() => setIsJs13kDropdownOpen(false), 300)
|
||||
}
|
||||
/>
|
||||
{isJs13kDropdownOpen && (
|
||||
<div className="js13k__dropdown">
|
||||
<button
|
||||
class="btn"
|
||||
style={{
|
||||
width: '200px',
|
||||
display: 'block',
|
||||
marginBottom: '16px'
|
||||
}}
|
||||
onClick={props.onJs13KDownloadBtnClick}
|
||||
>
|
||||
<Trans>Download game as zip</Trans>
|
||||
</button>
|
||||
<a
|
||||
class="btn"
|
||||
rel="noopener"
|
||||
style={{
|
||||
width: '200px',
|
||||
display: 'block',
|
||||
marginBottom: '16px'
|
||||
}}
|
||||
href="https://pasteboard.co/"
|
||||
target="_blank"
|
||||
>
|
||||
<Trans>Upload Image</Trans>
|
||||
</a>
|
||||
<button
|
||||
class="btn"
|
||||
style={{ width: '200px', display: 'block' }}
|
||||
onClick={props.onJs13KHelpBtnClick}
|
||||
>
|
||||
<Trans>Help</Trans>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div class="footer__right">
|
||||
<button
|
||||
onClick={props.saveHtmlBtnClickHandler}
|
||||
id="saveHtmlBtn"
|
||||
class="mode-btn hint--rounded hint--top-left hide-on-mobile hide-in-file-mode"
|
||||
aria-label={i18n._(t`Save as HTML file`)}
|
||||
>
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M5,20H19V18H5M19,9H15V3H9V9H5L12,16L19,9Z" />
|
||||
</svg>
|
||||
</button>
|
||||
<svg style="display: none;" xmlns="http://www.w3.org/2000/svg">
|
||||
<symbol id="codepen-logo" viewBox="0 0 120 120">
|
||||
<path
|
||||
class="outer-ring"
|
||||
d="M60.048 0C26.884 0 0 26.9 0 60.048s26.884 60 60 60.047c33.163 0 60.047-26.883 60.047-60.047 S93.211 0 60 0z M60.048 110.233c-27.673 0-50.186-22.514-50.186-50.186S32.375 9.9 60 9.9 c27.672 0 50.2 22.5 50.2 50.186S87.72 110.2 60 110.233z"
|
||||
/>
|
||||
<path
|
||||
class="inner-box"
|
||||
d="M97.147 48.319c-0.007-0.047-0.019-0.092-0.026-0.139c-0.016-0.09-0.032-0.18-0.056-0.268 c-0.014-0.053-0.033-0.104-0.05-0.154c-0.025-0.078-0.051-0.156-0.082-0.232c-0.021-0.053-0.047-0.105-0.071-0.156 c-0.033-0.072-0.068-0.143-0.108-0.211c-0.029-0.051-0.061-0.1-0.091-0.148c-0.043-0.066-0.087-0.131-0.135-0.193 c-0.035-0.047-0.072-0.094-0.109-0.139c-0.051-0.059-0.104-0.117-0.159-0.172c-0.042-0.043-0.083-0.086-0.127-0.125 c-0.059-0.053-0.119-0.104-0.181-0.152c-0.048-0.037-0.095-0.074-0.145-0.109c-0.019-0.012-0.035-0.027-0.053-0.039L61.817 23.5 c-1.072-0.715-2.468-0.715-3.54 0L24.34 46.081c-0.018 0.012-0.034 0.027-0.053 0.039c-0.05 0.035-0.097 0.072-0.144 0.1 c-0.062 0.049-0.123 0.1-0.181 0.152c-0.045 0.039-0.086 0.082-0.128 0.125c-0.056 0.055-0.108 0.113-0.158 0.2 c-0.038 0.045-0.075 0.092-0.11 0.139c-0.047 0.062-0.092 0.127-0.134 0.193c-0.032 0.049-0.062 0.098-0.092 0.1 c-0.039 0.068-0.074 0.139-0.108 0.211c-0.024 0.051-0.05 0.104-0.071 0.156c-0.031 0.076-0.057 0.154-0.082 0.2 c-0.017 0.051-0.035 0.102-0.05 0.154c-0.023 0.088-0.039 0.178-0.056 0.268c-0.008 0.047-0.02 0.092-0.025 0.1 c-0.019 0.137-0.029 0.275-0.029 0.416V71.36c0 0.1 0 0.3 0 0.418c0.006 0 0 0.1 0 0.1 c0.017 0.1 0 0.2 0.1 0.268c0.015 0.1 0 0.1 0.1 0.154c0.025 0.1 0.1 0.2 0.1 0.2 c0.021 0.1 0 0.1 0.1 0.154c0.034 0.1 0.1 0.1 0.1 0.213c0.029 0 0.1 0.1 0.1 0.1 c0.042 0.1 0.1 0.1 0.1 0.193c0.035 0 0.1 0.1 0.1 0.139c0.05 0.1 0.1 0.1 0.2 0.2 c0.042 0 0.1 0.1 0.1 0.125c0.058 0.1 0.1 0.1 0.2 0.152c0.047 0 0.1 0.1 0.1 0.1 c0.019 0 0 0 0.1 0.039L58.277 96.64c0.536 0.4 1.2 0.5 1.8 0.537c0.616 0 1.233-0.18 1.77-0.537 l33.938-22.625c0.018-0.012 0.034-0.027 0.053-0.039c0.05-0.035 0.097-0.072 0.145-0.109c0.062-0.049 0.122-0.1 0.181-0.152 c0.044-0.039 0.085-0.082 0.127-0.125c0.056-0.055 0.108-0.113 0.159-0.172c0.037-0.045 0.074-0.09 0.109-0.139 c0.048-0.062 0.092-0.127 0.135-0.193c0.03-0.049 0.062-0.098 0.091-0.146c0.04-0.07 0.075-0.141 0.108-0.213 c0.024-0.051 0.05-0.102 0.071-0.154c0.031-0.078 0.057-0.156 0.082-0.234c0.017-0.051 0.036-0.102 0.05-0.154 c0.023-0.088 0.04-0.178 0.056-0.268c0.008-0.045 0.02-0.092 0.026-0.137c0.018-0.139 0.028-0.277 0.028-0.418V48.735 C97.176 48.6 97.2 48.5 97.1 48.319z M63.238 32.073l25.001 16.666L77.072 56.21l-13.834-9.254V32.073z M56.856 32.1 v14.883L43.023 56.21l-11.168-7.471L56.856 32.073z M29.301 54.708l7.983 5.34l-7.983 5.34V54.708z M56.856 88.022L31.855 71.4 l11.168-7.469l13.833 9.252V88.022z M60.048 67.597l-11.286-7.549l11.286-7.549l11.285 7.549L60.048 67.597z M63.238 88.022V73.14 l13.834-9.252l11.167 7.469L63.238 88.022z M90.794 65.388l-7.982-5.34l7.982-5.34V65.388z"
|
||||
/>
|
||||
</symbol>
|
||||
</svg>
|
||||
|
||||
<button
|
||||
onClick={props.codepenBtnClickHandler}
|
||||
id="codepenBtn"
|
||||
class="mode-btn hint--rounded hint--top-left hide-on-mobile hide-in-file-mode"
|
||||
aria-label={i18n._(t`Edit on CodePen`)}
|
||||
>
|
||||
<svg>
|
||||
<use xlinkHref="#codepen-logo" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button
|
||||
id="screenshotBtn"
|
||||
class="mode-btn hint--rounded hint--top-left show-when-extension"
|
||||
onClick={props.screenshotBtnClickHandler}
|
||||
aria-label={i18n._(t`Take screenshot of preview`)}
|
||||
>
|
||||
<svg style="width:24px;height:24px" viewBox="0 0 24 24">
|
||||
<path d="M4,4H7L9,2H15L17,4H20A2,2 0 0,1 22,6V18A2,2 0 0,1 20,20H4A2,2 0 0,1 2,18V6A2,2 0 0,1 4,4M12,7A5,5 0 0,0 7,12A5,5 0 0,0 12,17A5,5 0 0,0 17,12A5,5 0 0,0 12,7M12,9A3,3 0 0,1 15,12A3,3 0 0,1 12,15A3,3 0 0,1 9,12A3,3 0 0,1 12,9Z" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div class="footer__separator hide-on-mobile" />
|
||||
|
||||
<button
|
||||
onClick={() => layoutBtnClickhandler(1)}
|
||||
id="layoutBtn1"
|
||||
class="mode-btn hide-on-mobile hide-in-file-mode"
|
||||
aria-label={i18n._(t`Switch to layout with preview on right`)}
|
||||
>
|
||||
<svg viewBox="0 0 100 100" style="transform:rotate(-90deg)">
|
||||
<use xlinkHref="#mode-icon" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => layoutBtnClickhandler(2)}
|
||||
id="layoutBtn2"
|
||||
class="mode-btn hide-on-mobile hide-in-file-mode"
|
||||
aria-label={i18n._(t`Switch to layout with preview on bottom`)}
|
||||
>
|
||||
<svg viewBox="0 0 100 100">
|
||||
<use xlinkHref="#mode-icon" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => layoutBtnClickhandler(3)}
|
||||
id="layoutBtn3"
|
||||
class="mode-btn hide-on-mobile hide-in-file-mode"
|
||||
aria-label={i18n._(t`Switch to layout with preview on left`)}
|
||||
>
|
||||
<svg viewBox="0 0 100 100" style="transform:rotate(90deg)">
|
||||
<use xlinkHref="#mode-icon" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => layoutBtnClickhandler(5)}
|
||||
id="layoutBtn5"
|
||||
class="mode-btn hide-on-mobile hide-in-file-mode"
|
||||
aria-label={i18n._(t`Switch to layout with all vertical panes`)}
|
||||
>
|
||||
<svg viewBox="0 0 100 100">
|
||||
<use xlinkHref="#vertical-mode-icon" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => layoutBtnClickhandler(4)}
|
||||
id="layoutBtn4"
|
||||
class="mode-btn hint--top-left hint--rounded hide-on-mobile"
|
||||
aria-label={i18n._(t`Toggle full screen preview`)}
|
||||
>
|
||||
<svg viewBox="0 0 100 100">
|
||||
<rect x="0" y="0" width="100" height="100" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="mode-btn hint--top-left hint--rounded hide-on-mobile"
|
||||
aria-label={i18n._(t`Detach preview`)}
|
||||
onClick={props.detachedPreviewBtnHandler}
|
||||
>
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M22,17V7H6V17H22M22,5A2,2 0 0,1 24,7V17C24,18.11 23.1,19 22,19H16V21H18V23H10V21H12V19H6C4.89,19 4,18.11 4,17V7A2,2 0 0,1 6,5H22M2,3V15H0V3A2,2 0 0,1 2,1H20V3H2Z" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div class="footer__separator" />
|
||||
|
||||
<button
|
||||
onClick={props.notificationsBtnClickHandler}
|
||||
id="notificationsBtn"
|
||||
class={`notifications-btn mode-btn hint--top-left hint--rounded ${
|
||||
props.hasUnseenChangelog ? 'has-new' : ''
|
||||
}`}
|
||||
aria-label={i18n._(t`See changelog`)}
|
||||
>
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M14,20A2,2 0 0,1 12,22A2,2 0 0,1 10,20H14M12,2A1,1 0 0,1 13,3V4.08C15.84,4.56 18,7.03 18,10V16L21,19H3L6,16V10C6,7.03 8.16,4.56 11,4.08V3A1,1 0 0,1 12,2Z" />
|
||||
</svg>
|
||||
<span class="notifications-btn__dot" />
|
||||
</button>
|
||||
<Button
|
||||
onClick={props.settingsBtnClickHandler}
|
||||
data-event-category="ui"
|
||||
data-event-action="settingsBtnClick"
|
||||
class="mode-btn hint--top-left hint--rounded"
|
||||
aria-label={i18n._(t`Settings`)}
|
||||
>
|
||||
<svg>
|
||||
<use xlinkHref="#settings-icon" />
|
||||
</svg>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</I18n>
|
||||
);
|
||||
};
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { h } from 'preact';
|
||||
import Modal from './Modal';
|
||||
import { Stack, VStack } from './Stack';
|
||||
import { Button } from './common';
|
||||
import { Trans } from '@lingui/macro';
|
||||
|
||||
@@ -7,15 +7,21 @@ export function HelpModal(props) {
|
||||
return (
|
||||
<Modal show={props.show} closeHandler={props.closeHandler}>
|
||||
<h1>
|
||||
<div class="web-maker-with-tag">Web Maker</div>
|
||||
<small style="font-size:14px;">{props.version}</small>
|
||||
<Stack gap={1} align="center">
|
||||
<div class="web-maker-with-tag">Web Maker</div>
|
||||
<span className="badge">{props.version}</span>
|
||||
</Stack>
|
||||
</h1>
|
||||
|
||||
<div>
|
||||
<p>
|
||||
<Trans>
|
||||
Made with <span style="margin-right: 8px;">💖</span>&{' '}
|
||||
<span style="margin-right: 8px;"> 🙌</span> by{' '}
|
||||
<span style="margin-right: 0.2rem;position:relative;top:-3px;">
|
||||
{' '}
|
||||
🙌
|
||||
</span>{' '}
|
||||
by{' '}
|
||||
<a
|
||||
href="https://twitter.com/chinchang457"
|
||||
target="_blank"
|
||||
@@ -56,7 +62,7 @@ export function HelpModal(props) {
|
||||
.
|
||||
</Trans>
|
||||
</p>
|
||||
<p>
|
||||
<p style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
|
||||
<Button
|
||||
onClick={props.onSupportBtnClick}
|
||||
data-event-action="supportDeveloperHelpBtnClick"
|
||||
@@ -190,29 +196,31 @@ export function HelpModal(props) {
|
||||
</details>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<VStack gap={1} align="stretch" fullWidth={true}>
|
||||
<h3>
|
||||
<Trans>License</Trans>
|
||||
</h3>
|
||||
<Trans>
|
||||
"Web Maker" is{' '}
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://github.com/chinchang/web-maker"
|
||||
>
|
||||
open-source
|
||||
</a>{' '}
|
||||
under the{' '}
|
||||
<a
|
||||
href="https://opensource.org/licenses/MIT"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
MIT License
|
||||
</a>
|
||||
</Trans>
|
||||
</p>
|
||||
<p>
|
||||
<Trans>
|
||||
"Web Maker" is{' '}
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://github.com/chinchang/web-maker"
|
||||
>
|
||||
open-source
|
||||
</a>{' '}
|
||||
under the{' '}
|
||||
<a
|
||||
href="https://opensource.org/licenses/MIT"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
MIT License
|
||||
</a>
|
||||
</Trans>
|
||||
</p>
|
||||
</VStack>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
|
@@ -1,5 +1,3 @@
|
||||
import { h } from 'preact';
|
||||
|
||||
export function Icons() {
|
||||
return (
|
||||
<svg
|
||||
@@ -109,6 +107,30 @@ export function Icons() {
|
||||
<symbol id="search" viewBox="0 0 24 24">
|
||||
<path d="M9.5,3A6.5,6.5 0 0,1 16,9.5C16,11.11 15.41,12.59 14.44,13.73L14.71,14H15.5L20.5,19L19,20.5L14,15.5V14.71L13.73,14.44C12.59,15.41 11.11,16 9.5,16A6.5,6.5 0 0,1 3,9.5A6.5,6.5 0 0,1 9.5,3M9.5,5C7,5 5,7 5,9.5C5,12 7,14 9.5,14C12,14 14,12 14,9.5C14,7 12,5 9.5,5Z" />
|
||||
</symbol>
|
||||
<symbol id="copy" viewBox="0 0 24 24">
|
||||
<path d="M19 21H8V7h11m0-2H8a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h11a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2m-3-4H4a2 2 0 0 0-2 2v14h2V3h12V1Z" />
|
||||
</symbol>
|
||||
<symbol id="trash" viewBox="0 0 24 24">
|
||||
<path d="M9 3v1H4v2h1v13a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V6h1V4h-5V3H9M7 6h10v13H7V6m2 2v9h2V8H9m4 0v9h2V8h-2Z" />
|
||||
</symbol>
|
||||
<symbol id="view-grid" viewBox="0 0 24 24">
|
||||
<path d="M3 11h8V3H3m0 18h8v-8H3m10 8h8v-8h-8m0-10v8h8V3" />
|
||||
</symbol>
|
||||
<symbol id="view-list" viewBox="0 0 24 24">
|
||||
<path d="M9,5V9H21V5M9,19H21V15H9M9,14H21V10H9M4,9H8V5H4M4,19H8V15H4M4,14H8V10H4V14Z" />
|
||||
</symbol>
|
||||
<symbol id="eye" viewBox="0 0 24 24">
|
||||
<path d="M12 9a3 3 0 0 0-3 3 3 3 0 0 0 3 3 3 3 0 0 0 3-3 3 3 0 0 0-3-3m0 8a5 5 0 0 1-5-5 5 5 0 0 1 5-5 5 5 0 0 1 5 5 5 5 0 0 1-5 5m0-12.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5Z" />
|
||||
</symbol>
|
||||
<symbol id="eye-striked" viewBox="0 0 24 24">
|
||||
<path d="M11.83 9 15 12.16V12a3 3 0 0 0-3-3h-.17m-4.3.8 1.55 1.55c-.05.21-.08.42-.08.65a3 3 0 0 0 3 3c.22 0 .44-.03.65-.08l1.55 1.55c-.67.33-1.41.53-2.2.53a5 5 0 0 1-5-5c0-.79.2-1.53.53-2.2M2 4.27l2.28 2.28.45.45C3.08 8.3 1.78 10 1 12c1.73 4.39 6 7.5 11 7.5 1.55 0 3.03-.3 4.38-.84l.43.42L19.73 22 21 20.73 3.27 3M12 7a5 5 0 0 1 5 5c0 .64-.13 1.26-.36 1.82l2.93 2.93c1.5-1.25 2.7-2.89 3.43-4.75-1.73-4.39-6-7.5-11-7.5-1.4 0-2.74.25-4 .7l2.17 2.15C10.74 7.13 11.35 7 12 7Z" />
|
||||
</symbol>
|
||||
<symbol id="check-circle" viewBox="0 0 24 24">
|
||||
<path d="M12 2C6.5 2 2 6.5 2 12S6.5 22 12 22 22 17.5 22 12 17.5 2 12 2M10 17L5 12L6.41 10.59L10 14.17L17.59 6.58L19 8L10 17Z" />
|
||||
</symbol>
|
||||
<symbol id="fork" viewBox="0 0 24 24">
|
||||
<path d="M13 14c-3.36 0-4.46 1.35-4.82 2.24C9.25 16.7 10 17.76 10 19a3 3 0 0 1-3 3 3 3 0 0 1-3-3c0-1.31.83-2.42 2-2.83V7.83A2.99 2.99 0 0 1 4 5a3 3 0 0 1 3-3 3 3 0 0 1 3 3c0 1.31-.83 2.42-2 2.83v5.29c.88-.65 2.16-1.12 4-1.12 2.67 0 3.56-1.34 3.85-2.23A3.006 3.006 0 0 1 14 7a3 3 0 0 1 3-3 3 3 0 0 1 3 3c0 1.34-.88 2.5-2.09 2.86C17.65 11.29 16.68 14 13 14m-6 4a1 1 0 0 0-1 1 1 1 0 0 0 1 1 1 1 0 0 0 1-1 1 1 0 0 0-1-1M7 4a1 1 0 0 0-1 1 1 1 0 0 0 1 1 1 1 0 0 0 1-1 1 1 0 0 0-1-1m10 2a1 1 0 0 0-1 1 1 1 0 0 0 1 1 1 1 0 0 0 1-1 1 1 0 0 0-1-1Z" />
|
||||
</symbol>
|
||||
<symbol id="loader-icon" viewBox="0 0 44 44">
|
||||
{/* By Sam Herbert (@sherb), for everyone. More http://goo.gl/7AJzbL */}
|
||||
<g fill="none" fillRule="evenodd" strokeWidth={10}>
|
||||
@@ -162,3 +184,17 @@ export function Icons() {
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export const Icon = ({ name, color = 'currentColor', size, ...rest }) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
style={{ fill: color }}
|
||||
{...rest}
|
||||
>
|
||||
<use xlinkHref={`#${name}`} />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
@@ -1,14 +1,18 @@
|
||||
import { h } from 'preact';
|
||||
import { getHumanDate } from '../utils';
|
||||
import Modal from './Modal';
|
||||
import { HStack, Stack } from './Stack';
|
||||
import { Icon } from './Icons';
|
||||
|
||||
export function ItemTile({
|
||||
item,
|
||||
onClick,
|
||||
onForkBtnClick,
|
||||
onRemoveBtnClick,
|
||||
onToggleVisibilityBtnClick,
|
||||
focusable,
|
||||
inline
|
||||
inline,
|
||||
hasOptions = true
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
@@ -27,6 +31,13 @@ export function ItemTile({
|
||||
aria-label="Creates a duplicate of this creation (Ctrl/⌘ + F)"
|
||||
onClick={onForkBtnClick}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M13 14c-3.36 0-4.46 1.35-4.82 2.24C9.25 16.7 10 17.76 10 19a3 3 0 0 1-3 3 3 3 0 0 1-3-3c0-1.31.83-2.42 2-2.83V7.83A2.99 2.99 0 0 1 4 5a3 3 0 0 1 3-3 3 3 0 0 1 3 3c0 1.31-.83 2.42-2 2.83v5.29c.88-.65 2.16-1.12 4-1.12 2.67 0 3.56-1.34 3.85-2.23A3.006 3.006 0 0 1 14 7a3 3 0 0 1 3-3 3 3 0 0 1 3 3c0 1.34-.88 2.5-2.09 2.86C17.65 11.29 16.68 14 13 14m-6 4a1 1 0 0 0-1 1 1 1 0 0 0 1 1 1 1 0 0 0 1-1 1 1 0 0 0-1-1M7 4a1 1 0 0 0-1 1 1 1 0 0 0 1 1 1 1 0 0 0 1-1 1 1 0 0 0-1-1m10 2a1 1 0 0 0-1 1 1 1 0 0 0 1 1 1 1 0 0 0 1-1 1 1 0 0 0-1-1Z" />
|
||||
</svg>
|
||||
Fork<span class="show-when-selected">(Ctrl/⌘ + F)</span>
|
||||
</button>
|
||||
) : null}
|
||||
@@ -36,13 +47,19 @@ export function ItemTile({
|
||||
aria-label="Remove"
|
||||
onClick={onRemoveBtnClick}
|
||||
>
|
||||
X
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M9 3v1H4v2h1v13a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V6h1V4h-5V3H9M7 6h10v13H7V6m2 2v9h2V8H9m4 0v9h2V8h-2Z" />
|
||||
</svg>
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex flex-v-center">
|
||||
{item.img ? (
|
||||
<div>
|
||||
<div class="d-f">
|
||||
<img
|
||||
class="saved-item-tile__img"
|
||||
height="40"
|
||||
@@ -66,12 +83,32 @@ export function ItemTile({
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
{item.updatedOn ? (
|
||||
{hasOptions && (
|
||||
<div class="saved-item-tile__meta">
|
||||
Last updated:{' '}
|
||||
<time dateTime={item.updatedOn}>{getHumanDate(item.updatedOn)}</time>
|
||||
<HStack justify="space-between">
|
||||
<div>
|
||||
{item.updatedOn ? (
|
||||
<>
|
||||
Last updated:{' '}
|
||||
<time dateTime={item.updatedOn}>
|
||||
{getHumanDate(item.updatedOn)}
|
||||
</time>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
<div>
|
||||
<Stack gap={1} align="center">
|
||||
<Icon
|
||||
size="16"
|
||||
color="currentColor"
|
||||
name={item.isPublic ? 'eye' : 'eye-striked'}
|
||||
/>
|
||||
{item.isPublic ? 'Public' : ''}
|
||||
</Stack>
|
||||
</div>
|
||||
</HStack>
|
||||
</div>
|
||||
) : null}
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
52
src/components/Loader.jsx
Normal file
52
src/components/Loader.jsx
Normal file
@@ -0,0 +1,52 @@
|
||||
export function Loader({ height, noMargin, leftMargin }) {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 166 166"
|
||||
height={height || '1.6em'}
|
||||
style={{
|
||||
margin: noMargin
|
||||
? null
|
||||
: leftMargin
|
||||
? ` 0 0 0 ${leftMargin}`
|
||||
: '0 0.8rem'
|
||||
}}
|
||||
class="new-loader"
|
||||
>
|
||||
<g fill="none" fillRule="evenodd">
|
||||
<path
|
||||
d="M83 166c-45.84 0-83-37.16-83-83S37.16 0 83 0s83 37.16 83 83-37.16 83-83 83zm0-29c29.823 0 54-24.177 54-54s-24.177-54-54-54-54 24.177-54 54 24.177 54 54 54z"
|
||||
fill="currentColor"
|
||||
style={{ opacity: 0.2 }}
|
||||
/>
|
||||
<path
|
||||
d="M137.008 83H137c0-29.823-24.177-54-54-54S29 53.177 29 83h-.008c.005.166.008.333.008.5C29 91.508 22.508 98 14.5 98S0 91.508 0 83.5c0-.167.003-.334.008-.5H0C0 37.16 37.16 0 83 0s83 37.16 83 83h-.008c.005.166.008.333.008.5 0 8.008-6.492 14.5-14.5 14.5S137 91.508 137 83.5c0-.167.003-.334.008-.5z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<animateTransform
|
||||
attributeName="transform"
|
||||
attributeType="XML"
|
||||
type="rotate"
|
||||
dur="1s"
|
||||
from="0 83 83"
|
||||
to="360 83 83"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function LoaderWithText({ children }) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
margin: '2rem 1rem'
|
||||
}}
|
||||
>
|
||||
<Loader /> {children}
|
||||
</div>
|
||||
);
|
||||
}
|
@@ -54,21 +54,8 @@ export default class Login extends Component {
|
||||
Login with Google
|
||||
</button>
|
||||
</p>
|
||||
<p class="mb-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={this.login.bind(this)}
|
||||
class="social-login-btn social-login-btn--facebook btn btn-icon btn--big full-width hint--right hint--always"
|
||||
data-auth-provider="facebook"
|
||||
data-hint="You logged in with Facebook last time"
|
||||
>
|
||||
<svg>
|
||||
<use xlinkHref="#fb-icon" />
|
||||
</svg>
|
||||
Login with Facebook (deprecated)
|
||||
</button>
|
||||
</p>
|
||||
<p>Join a community of 50,000+ Developers</p>
|
||||
|
||||
<p>Join a community of 70,000+ Developers</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@@ -2,16 +2,32 @@ import { h } from 'preact';
|
||||
import { Button } from './common';
|
||||
import { Trans, NumberFormat, t } from '@lingui/macro';
|
||||
import { I18n } from '@lingui/react';
|
||||
import { ProBadge } from './ProBadge';
|
||||
import { HStack, Stack } from './Stack';
|
||||
import { Icon } from './Icons';
|
||||
|
||||
const DEFAULT_PROFILE_IMG =
|
||||
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23ccc' d='M12,19.2C9.5,19.2 7.29,17.92 6,16C6.03,14 10,12.9 12,12.9C14,12.9 17.97,14 18,16C16.71,17.92 14.5,19.2 12,19.2M12,5A3,3 0 0,1 15,8A3,3 0 0,1 12,11A3,3 0 0,1 9,8A3,3 0 0,1 12,5M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12C22,6.47 17.5,2 12,2Z'/%3E%3C/svg%3E";
|
||||
|
||||
export function MainHeader(props) {
|
||||
export function MainHeader({
|
||||
user,
|
||||
currentItem,
|
||||
titleInputBlurHandler,
|
||||
runBtnClickHandler,
|
||||
assetsBtnHandler,
|
||||
isFileMode,
|
||||
onItemFork,
|
||||
...props
|
||||
}) {
|
||||
const isAutoPreviewOn =
|
||||
window.forcedSettings.autoPreview !== undefined
|
||||
? window.forcedSettings
|
||||
: props.isAutoPreviewOn;
|
||||
|
||||
const isNotMine =
|
||||
currentItem.createdBy && user?.uid !== currentItem.createdBy;
|
||||
|
||||
// console.log(33, currentItem, user?.uid);
|
||||
return (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
@@ -21,15 +37,15 @@ export function MainHeader(props) {
|
||||
id="titleInput"
|
||||
title="Click to edit"
|
||||
class="item-title-input"
|
||||
value={props.title}
|
||||
onBlur={props.titleInputBlurHandler}
|
||||
value={currentItem.title}
|
||||
onBlur={titleInputBlurHandler}
|
||||
/>
|
||||
<div class="main-header__btn-wrap flex flex-v-center">
|
||||
{!isAutoPreviewOn && (
|
||||
<button
|
||||
class="btn btn btn--dark flex flex-v-center hint--rounded hint--bottom-left"
|
||||
aria-label={i18n._(t`Run preview (Ctrl/⌘ + Shift + 5)`)}
|
||||
onClick={props.runBtnClickHandler}
|
||||
onClick={runBtnClickHandler}
|
||||
>
|
||||
<svg>
|
||||
<use xlinkHref="#play-icon" />
|
||||
@@ -37,8 +53,17 @@ export function MainHeader(props) {
|
||||
<Trans>Run</Trans>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{!props.isFileMode && (
|
||||
<Button
|
||||
onClick={assetsBtnHandler}
|
||||
data-event-category="ui"
|
||||
data-event-action="addLibraryButtonClick"
|
||||
data-testid="addLibraryButton"
|
||||
class="btn btn--dark hint--rounded hint--bottom-left"
|
||||
aria-label={i18n._(t`Upload/Use assets`)}
|
||||
>
|
||||
<Trans>Assets</Trans>
|
||||
</Button>
|
||||
{!isFileMode && (
|
||||
<Button
|
||||
onClick={props.addLibraryBtnHandler}
|
||||
data-event-category="ui"
|
||||
@@ -59,6 +84,35 @@ export function MainHeader(props) {
|
||||
</span>
|
||||
</Button>
|
||||
)}
|
||||
<button
|
||||
class="btn btn--dark hint--bottom-left"
|
||||
aria-label={i18n._(t`Share this creation publicly`)}
|
||||
data-testid="newButton"
|
||||
onClick={props.shareBtnHandler}
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
style={{
|
||||
fill: currentItem.isPublic ? 'limegreen' : 'currentColor'
|
||||
}}
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M18 16.08C17.24 16.08 16.56 16.38 16.04 16.85L8.91 12.7C8.96 12.47 9 12.24 9 12S8.96 11.53 8.91 11.3L15.96 7.19C16.5 7.69 17.21 8 18 8C19.66 8 21 6.66 21 5S19.66 2 18 2 15 3.34 15 5C15 5.24 15.04 5.47 15.09 5.7L8.04 9.81C7.5 9.31 6.79 9 6 9C4.34 9 3 10.34 3 12S4.34 15 6 15C6.79 15 7.5 14.69 8.04 14.19L15.16 18.34C15.11 18.55 15.08 18.77 15.08 19C15.08 20.61 16.39 21.91 18 21.91S20.92 20.61 20.92 19C20.92 17.39 19.61 16.08 18 16.08M18 4C18.55 4 19 4.45 19 5S18.55 6 18 6 17 5.55 17 5 17.45 4 18 4M6 13C5.45 13 5 12.55 5 12S5.45 11 6 11 7 11.45 7 12 6.55 13 6 13M18 20C17.45 20 17 19.55 17 19S17.45 18 18 18 19 18.45 19 19 18.55 20 18 20Z" />
|
||||
</svg>
|
||||
{currentItem.isPublic ? null : <Trans>Share</Trans>}
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="btn btn--dark hint--bottom-left"
|
||||
aria-label={i18n._(t`Fork this creation`)}
|
||||
data-testid="headerForkButton"
|
||||
onClick={onItemFork}
|
||||
>
|
||||
<Icon name="fork" />
|
||||
<Trans>Fork</Trans>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="btn btn--dark hint--rounded hint--bottom-left"
|
||||
@@ -71,22 +125,25 @@ export function MainHeader(props) {
|
||||
</svg>
|
||||
<Trans>New</Trans>
|
||||
</button>
|
||||
<button
|
||||
id="saveBtn"
|
||||
class={`btn btn--dark hint--rounded hint--bottom-left ${
|
||||
props.isSaving ? 'is-loading' : ''
|
||||
} ${props.unsavedEditCount ? 'is-marked' : 0}`}
|
||||
aria-label={i18n._(t`Save current creation (Ctrl/⌘ + S)`)}
|
||||
onClick={props.saveBtnHandler}
|
||||
>
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M15,9H5V5H15M12,19A3,3 0 0,1 9,16A3,3 0 0,1 12,13A3,3 0 0,1 15,16A3,3 0 0,1 12,19M17,3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V7L17,3Z" />
|
||||
</svg>
|
||||
<svg class="btn-loader" width="15" height="15" stroke="#fff">
|
||||
<use xlinkHref="#loader-icon" />
|
||||
</svg>
|
||||
<Trans>Save</Trans>
|
||||
</button>
|
||||
|
||||
{!isNotMine && (
|
||||
<button
|
||||
id="saveBtn"
|
||||
class={`btn btn--dark hint--rounded hint--bottom-left ${
|
||||
props.isSaving ? 'is-loading' : ''
|
||||
} ${props.unsavedEditCount ? 'is-marked' : 0}`}
|
||||
aria-label={i18n._(t`Save current creation (Ctrl/⌘ + S)`)}
|
||||
onClick={props.saveBtnHandler}
|
||||
>
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M15,9H5V5H15M12,19A3,3 0 0,1 9,16A3,3 0 0,1 12,13A3,3 0 0,1 15,16A3,3 0 0,1 12,19M17,3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V7L17,3Z" />
|
||||
</svg>
|
||||
<svg class="btn-loader" width="15" height="15" stroke="#fff">
|
||||
<use xlinkHref="#loader-icon" />
|
||||
</svg>
|
||||
<Trans>Save</Trans>
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
id="openItemsBtn"
|
||||
class={`btn btn--dark hint--rounded hint--bottom-left ${
|
||||
@@ -103,13 +160,13 @@ export function MainHeader(props) {
|
||||
</svg>
|
||||
<Trans>Open</Trans>
|
||||
</button>
|
||||
{!props.user ? (
|
||||
{!user ? (
|
||||
<Button
|
||||
onClick={props.loginBtnHandler}
|
||||
data-event-category="ui"
|
||||
data-event-action="loginButtonClick"
|
||||
data-testid="loginButton"
|
||||
class="btn btn--dark hint--rounded hint--bottom-left"
|
||||
class="btn btn--dark"
|
||||
>
|
||||
<Trans>Login/Signup</Trans>
|
||||
</Button>
|
||||
@@ -121,14 +178,15 @@ export function MainHeader(props) {
|
||||
aria-label={i18n._(t`See profile or Logout`)}
|
||||
class="btn--dark hint--rounded hint--bottom-left"
|
||||
>
|
||||
<img
|
||||
id="headerAvatarImg"
|
||||
width="20"
|
||||
src={
|
||||
props.user ? props.user.photoURL || DEFAULT_PROFILE_IMG : ''
|
||||
}
|
||||
class="main-header__avatar-img"
|
||||
/>
|
||||
<HStack gap={1}>
|
||||
<img
|
||||
id="headerAvatarImg"
|
||||
width="20"
|
||||
src={user ? user.photoURL || DEFAULT_PROFILE_IMG : ''}
|
||||
class="main-header__avatar-img"
|
||||
/>
|
||||
{user?.isPro ? <ProBadge /> : null}
|
||||
</HStack>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
@@ -77,31 +77,47 @@ const Modal = ({
|
||||
if (!show) return null;
|
||||
|
||||
return (
|
||||
<Portal into={`body`}>
|
||||
<div
|
||||
role="dialog"
|
||||
class={`${extraClasses || ''} modal is-modal-visible ${
|
||||
small ? 'modal--small' : ''
|
||||
}`}
|
||||
ref={overlayRef}
|
||||
onClick={onOverlayClick}
|
||||
>
|
||||
<div class="modal__content">
|
||||
{hideCloseButton ? null : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={closeHandler}
|
||||
aria-label="Close modal"
|
||||
data-testid="closeModalButton"
|
||||
title="Close"
|
||||
class="js-modal__close-btn modal__close-btn"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
)}
|
||||
{children}
|
||||
<Portal into={`#portal`}>
|
||||
<>
|
||||
{/* <div class="modal-overlay" /> */}
|
||||
<div
|
||||
role="dialog"
|
||||
class={`${extraClasses || ''} modal is-modal-visible ${
|
||||
small ? 'modal--small' : ''
|
||||
}
|
||||
${noOverlay ? 'modal--no-overlay' : ''}
|
||||
`}
|
||||
ref={overlayRef}
|
||||
onClick={onOverlayClick}
|
||||
>
|
||||
<div class="modal__content">
|
||||
{hideCloseButton ? null : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={closeHandler}
|
||||
aria-label="Close modal"
|
||||
data-testid="closeModalButton"
|
||||
title="Close"
|
||||
class="js-modal__close-btn dialog__close-btn modal__close-btn"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="3.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
@@ -74,11 +74,11 @@ function Notification({ version, isLatest, ...props }) {
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
Web Maker now has more than 60K weekly active users! Thank you for
|
||||
Web Maker now has more than 70K weekly active users! Thank you for
|
||||
being a part of this community of awesome developers. If you find
|
||||
Web Maker helpful,{' '}
|
||||
<a
|
||||
href="https://chrome.google.com/webstore/detail/web-maker/lkfkkhfhhdkiemehlpkgjeojomhpccnh/reviews"
|
||||
href="https://chromewebstore.google.com/detail/web-maker/lkfkkhfhhdkiemehlpkgjeojomhpccnh/reviews"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="btn"
|
||||
@@ -87,23 +87,13 @@ function Notification({ version, isLatest, ...props }) {
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="http://twitter.com/share?url=https://webmaker.app/&text=Web Maker - A blazing fast %26 offline web playground! via @webmakerApp&related=webmakerApp&hashtags=web,editor,chrome,extension"
|
||||
href="http://twitter.com/share?url=https://webmaker.app/&text=Web Maker - A blazing fast %26 offline frontend playground! via @webmakerApp&related=webmakerApp&hashtags=web,editor,chrome,extension"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="btn"
|
||||
>
|
||||
Share it
|
||||
</a>
|
||||
|
||||
<Button
|
||||
aria-label="Support the developer"
|
||||
onClick={props.onSupportBtnClick}
|
||||
data-event-action="supportDeveloperChangelogBtnClick"
|
||||
data-event-category="ui"
|
||||
class="btn btn-icon"
|
||||
>
|
||||
Support the developer
|
||||
</Button>
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
@@ -114,6 +104,36 @@ export function Notifications(props) {
|
||||
return (
|
||||
<div>
|
||||
<h1>Whats new?</h1>
|
||||
<Notification version="6.0.0" {...props} isLatest={true}>
|
||||
<li>
|
||||
<strong>🎁 PRO plan 🎉</strong>: Today I introduce to use Web Maker's
|
||||
PRO plan! A set of additional super-features which you can buy. The
|
||||
PRO plan is available as monthly/annual subscription as well as a
|
||||
one-time lifetime price! Let's see what you get as a PRO.
|
||||
</li>
|
||||
<li>
|
||||
<strong>🔓 Share your creations</strong>: Web Maker has always been a
|
||||
privacy-first app. Continuing that culture, today we introduce "Share
|
||||
your creation" feature. Your creations are still created as private
|
||||
but now you can securely make them public to share with the world. As
|
||||
a free user you can have 1 creation public at a time. Upgrading to PRO
|
||||
gives you unlimited public creations.
|
||||
</li>
|
||||
<li>
|
||||
<strong>🗄️ Asset hosting</strong>: No more going to other places in
|
||||
order to host your images, CSS or JS files. Web Maker PRO gives you
|
||||
the ability to host your assets right inside Web Maker. You can upload
|
||||
images, CSS and JS files and use them in your creations.
|
||||
</li>
|
||||
<li>
|
||||
<strong>📁 Files mode</strong>: As a free user you could always create
|
||||
2 creations in Files mode. With PRO, you can create unlimited
|
||||
creations in Files mode.
|
||||
</li>
|
||||
<NotificationItem type="ui">
|
||||
Fork button is now available in the header too
|
||||
</NotificationItem>
|
||||
</Notification>
|
||||
<Notification version="5.3.0" {...props} isLatest={true}>
|
||||
<li>
|
||||
<strong>Tailwind CSS templates</strong>: Tailwind CSS template is now
|
||||
|
31
src/components/Panel.jsx
Normal file
31
src/components/Panel.jsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { forwardRef } from 'preact/compat';
|
||||
|
||||
export const Panel = forwardRef(function Panel(
|
||||
{
|
||||
classes = '',
|
||||
padding = '2rem',
|
||||
fullWidth = true,
|
||||
fullHeight = false,
|
||||
glowing = false,
|
||||
topFocus = false,
|
||||
onlyBorder = false,
|
||||
children
|
||||
},
|
||||
ref
|
||||
) {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
style={{
|
||||
padding: padding,
|
||||
width: fullWidth ? '100%' : 'auto',
|
||||
height: fullHeight ? '100%' : 'auto'
|
||||
}}
|
||||
className={`panel ${classes} ${glowing && 'panelGlowing'} ${
|
||||
topFocus && 'panelTopFocus'
|
||||
} ${onlyBorder && 'panelOnlyBorder'}`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
});
|
134
src/components/Pro.jsx
Normal file
134
src/components/Pro.jsx
Normal file
@@ -0,0 +1,134 @@
|
||||
import { useEffect, useState } from 'preact/hooks';
|
||||
import { ProBadge } from './ProBadge';
|
||||
import { HStack, Stack, VStack } from './Stack';
|
||||
import Switch from './Switch';
|
||||
import { alertsService } from '../notifications';
|
||||
import { A, Button } from './common';
|
||||
import { useCheckout } from '../hooks/useCheckout';
|
||||
import { Text } from './Text';
|
||||
import { Icon } from './Icons';
|
||||
import { showConfetti } from '../utils';
|
||||
|
||||
const checkoutIds = {
|
||||
monthly: '1601bc00-9623-435b-b1de-2a70a2445c13',
|
||||
annual: 'aae95d78-05c8-46f5-b11e-2d40ddd211d3',
|
||||
generic: 'd1d2da1a-ae8f-4222-bbaf-6e07da8954bf' //'f8c64e50-7734-438b-a122-3510156f14ed'
|
||||
};
|
||||
export function Pro({ user, onLoginClick }) {
|
||||
const hasCheckoutLoaded = useCheckout();
|
||||
const [isAnnual, setIsAnnual] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasCheckoutLoaded) {
|
||||
window.LemonSqueezy.Setup({
|
||||
eventHandler: e => {
|
||||
console.log('eventHandler', e);
|
||||
if (e.event === 'Checkout.Success') {
|
||||
showConfetti(2);
|
||||
alertsService.add(
|
||||
'You are now PRO! 🎉. Reloading your superpowers...'
|
||||
);
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
});
|
||||
window.LemonSqueezy.Refresh();
|
||||
}
|
||||
}, [hasCheckoutLoaded]);
|
||||
return (
|
||||
<VStack gap={2} align="stretch">
|
||||
{/* <Stack justify="center">
|
||||
<Switch
|
||||
labels={['Monthly', 'Annually']}
|
||||
checked={isAnnual}
|
||||
showBothLabels={true}
|
||||
onChange={e => {
|
||||
setIsAnnual(e.target.checked);
|
||||
}}
|
||||
/>
|
||||
</Stack> */}
|
||||
<Stack gap={2} align="stretch">
|
||||
<Card
|
||||
price="Free"
|
||||
name="Starter"
|
||||
features={[
|
||||
'Unlimited private creations',
|
||||
'1 Public creation',
|
||||
'2 Files mode creations'
|
||||
]}
|
||||
/>
|
||||
<Card
|
||||
bg="#674dad"
|
||||
price={'Starting $6/mo'}
|
||||
name="Pro"
|
||||
action={
|
||||
window.user ? (
|
||||
<A
|
||||
class="btn btn--pro lemonsqueezy-button d-f jc-c ai-c"
|
||||
style="gap:0.2rem"
|
||||
href={`https://web-maker.lemonsqueezy.com/checkout/buy/${checkoutIds.generic}?embed=1&checkout[custom][userId]=${user?.uid}`}
|
||||
>
|
||||
Go PRO
|
||||
</A>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn--pro jc-c"
|
||||
onClick={onLoginClick}
|
||||
>
|
||||
Login & upgrade to PRO
|
||||
</button>
|
||||
)
|
||||
}
|
||||
features={[
|
||||
'Unlimited private creations',
|
||||
'Unlimited public creations',
|
||||
'Unlimited files mode creations',
|
||||
'Asset hosting',
|
||||
'Priority support',
|
||||
'No Ads'
|
||||
]}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack justify="center">
|
||||
<Text tag="p" appearance="secondary">
|
||||
30 days refund policy if not satisfied.
|
||||
</Text>
|
||||
</Stack>
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
|
||||
const Card = ({ bg, name, price, action, features }) => {
|
||||
return (
|
||||
<div class="plan-card" style={{ background: bg }}>
|
||||
<VStack gap={2} align="stretch" justify="flex-start">
|
||||
<VStack gap={0} align="stretch" justify="flex-start">
|
||||
<Text transform="uppercase" weight="600">
|
||||
{name}
|
||||
</Text>
|
||||
<Text size="5" weight="800" appearance="primary">
|
||||
{' '}
|
||||
{price}
|
||||
</Text>
|
||||
</VStack>
|
||||
{action}
|
||||
{!action && (
|
||||
<a class="btn" aria-hidden="true" style="visibility:hidden">
|
||||
s
|
||||
</a>
|
||||
)}
|
||||
<VStack gap={1} align="flex-start">
|
||||
{features.map(f => (
|
||||
<HStack gap={1} align="center">
|
||||
<Icon name="check-circle" size="1.4rem" />
|
||||
{f}
|
||||
</HStack>
|
||||
))}
|
||||
</VStack>
|
||||
</VStack>
|
||||
</div>
|
||||
);
|
||||
};
|
3
src/components/ProBadge.jsx
Normal file
3
src/components/ProBadge.jsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export const ProBadge = () => {
|
||||
return <div className="badge pro-badge">PRO</div>;
|
||||
};
|
@@ -1,30 +1,158 @@
|
||||
import { h } from 'preact';
|
||||
import { useState, useEffect } from 'preact/hooks';
|
||||
import { ProBadge } from './ProBadge';
|
||||
import { HStack, Stack, VStack } from './Stack';
|
||||
import { Panel } from './Panel';
|
||||
import { Text } from './Text';
|
||||
import { getHumanReadableDate } from '../utils';
|
||||
import { LoaderWithText } from './Loader';
|
||||
|
||||
const DEFAULT_PROFILE_IMG =
|
||||
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23ccc' d='M12,19.2C9.5,19.2 7.29,17.92 6,16C6.03,14 10,12.9 12,12.9C14,12.9 17.97,14 18,16C16.71,17.92 14.5,19.2 12,19.2M12,5A3,3 0 0,1 15,8A3,3 0 0,1 12,11A3,3 0 0,1 9,8A3,3 0 0,1 12,5M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12C22,6.47 17.5,2 12,2Z'/%3E%3C/svg%3E";
|
||||
|
||||
export function Profile({ user, logoutBtnHandler }) {
|
||||
const Header = ({ user, logoutBtnHandler }) => {
|
||||
return (
|
||||
<div class="tac">
|
||||
<img
|
||||
height="80"
|
||||
class="profile-modal__avatar-img"
|
||||
src={user ? user.photoURL || DEFAULT_PROFILE_IMG : ''}
|
||||
id="profileAvatarImg"
|
||||
alt="Profile image"
|
||||
/>
|
||||
<h3 id="profileUserName" class="mb-2">
|
||||
{user && user.displayName ? user.displayName : 'Anonymous Creator'}
|
||||
</h3>
|
||||
<p>
|
||||
<button
|
||||
class="btn"
|
||||
aria-label="Logout from your account"
|
||||
onClick={logoutBtnHandler}
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
<Stack gap={5}>
|
||||
<Stack gap={2} align="center">
|
||||
<img
|
||||
height="80"
|
||||
class={`profile-modal__avatar-img ${user?.isPro ? 'is-pro' : ''}`}
|
||||
src={user ? user.photoURL || DEFAULT_PROFILE_IMG : ''}
|
||||
id="profileAvatarImg"
|
||||
alt="Profile image"
|
||||
/>
|
||||
|
||||
<VStack gap={1} align="flex-start">
|
||||
<h3
|
||||
class={`profile-modal__name ${user?.isPro ? 's-pro' : ''}`}
|
||||
id="profileUserName"
|
||||
>
|
||||
{user && user.displayName ? user.displayName : 'Anonymous Creator'}
|
||||
</h3>
|
||||
{user.isPro && <ProBadge />}
|
||||
</VStack>
|
||||
</Stack>
|
||||
|
||||
<button
|
||||
class="btn btn--primary"
|
||||
aria-label="Logout from your account"
|
||||
onClick={logoutBtnHandler}
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export function Profile({ user, logoutBtnHandler }) {
|
||||
const [currentSubscription, setCurrentSubscription] = useState(null);
|
||||
const [isFetchingSubscription, setIsFetchingSubscription] = useState(false);
|
||||
useEffect(() => {
|
||||
if (user?.isPro) {
|
||||
setIsFetchingSubscription(true);
|
||||
window.db.getUserSubscriptionEvents(user.uid).then(events => {
|
||||
setIsFetchingSubscription(false);
|
||||
|
||||
let creationEvent = events
|
||||
.filter(
|
||||
event =>
|
||||
event.type === 'subscription_created' ||
|
||||
event.type === 'order_created'
|
||||
)
|
||||
.sort((a, b) => b.timestamp.seconds - a.timestamp.seconds)
|
||||
// remove order_created events which correspond to subscriptions (non lifetime orders)
|
||||
.filter(
|
||||
event =>
|
||||
!(
|
||||
event.type === 'order_created' &&
|
||||
!event.data.data.attributes.first_order_item?.variant_name?.match(
|
||||
/lifetime/
|
||||
)
|
||||
)
|
||||
)[0];
|
||||
if (creationEvent) {
|
||||
console.log(creationEvent);
|
||||
creationEvent.attributes = creationEvent.data.data.attributes;
|
||||
setCurrentSubscription(creationEvent);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [user]);
|
||||
return (
|
||||
<VStack gap={4}>
|
||||
<Header user={user} logoutBtnHandler={logoutBtnHandler} />
|
||||
{window.user?.isPro && (
|
||||
<Panel>
|
||||
{isFetchingSubscription ? (
|
||||
<LoaderWithText>Loading billing details...</LoaderWithText>
|
||||
) : null}
|
||||
{currentSubscription ? (
|
||||
<VStack align="stretch" gap={1}>
|
||||
<Text>
|
||||
Plan:
|
||||
<Text weight="700">
|
||||
{' '}
|
||||
Web Maker PRO (
|
||||
{currentSubscription.attributes.variant_name ||
|
||||
currentSubscription.attributes.first_order_item
|
||||
?.variant_name}
|
||||
)
|
||||
</Text>
|
||||
</Text>
|
||||
<Text>
|
||||
Subscription Status:{' '}
|
||||
<Text weight="700">
|
||||
{currentSubscription.attributes.status === 'paid'
|
||||
? 'PRO for life ❤️'
|
||||
: currentSubscription.attributes.status}
|
||||
</Text>
|
||||
</Text>
|
||||
|
||||
<Text>
|
||||
Renews on:{' '}
|
||||
<Text weight="700">
|
||||
{currentSubscription.attributes.status === 'paid'
|
||||
? 'Never ever'
|
||||
: getHumanReadableDate(
|
||||
currentSubscription.attributes.renews_at
|
||||
)}
|
||||
</Text>
|
||||
</Text>
|
||||
|
||||
{currentSubscription.attributes.status === 'paid' ? null : (
|
||||
<a
|
||||
target="_blank"
|
||||
href={currentSubscription.attributes.urls.customer_portal}
|
||||
>
|
||||
Cancel subscription
|
||||
</a>
|
||||
)}
|
||||
{/* <a
|
||||
target="_blank"
|
||||
href={
|
||||
currentSubscription.attributes.urls
|
||||
.customer_portal_update_subscription
|
||||
}
|
||||
>
|
||||
Link 2
|
||||
</a>
|
||||
<a
|
||||
target="_blank"
|
||||
href={currentSubscription.attributes.urls.update_payment_method}
|
||||
>
|
||||
Link 3
|
||||
</a> */}
|
||||
</VStack>
|
||||
) : null}
|
||||
</Panel>
|
||||
)}
|
||||
{user?.isPro && currentSubscription ? (
|
||||
<img
|
||||
class="profile-modal__panda"
|
||||
src="assets/pro-panda.png"
|
||||
width="300"
|
||||
style="position:absolute;bottom:-3rem; right: -7rem;"
|
||||
/>
|
||||
) : null}
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
|
@@ -157,11 +157,22 @@ export default function SavedItemPane({
|
||||
>
|
||||
<button
|
||||
onClick={onCloseIntent}
|
||||
class="btn saved-items-pane__close-btn"
|
||||
class="btn dialog__close-btn saved-items-pane__close-btn"
|
||||
id="js-saved-items-pane-close-btn"
|
||||
aria-label={i18n._(t`Close saved creations pane`)}
|
||||
>
|
||||
X
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="3.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
<div
|
||||
class="flex flex-v-center"
|
||||
@@ -215,6 +226,9 @@ export default function SavedItemPane({
|
||||
onClick={() => itemClickHandler(item)}
|
||||
onForkBtnClick={e => itemForkBtnClickHandler(item, e)}
|
||||
onRemoveBtnClick={e => itemRemoveBtnClickHandler(item, e)}
|
||||
onToggleVisibilityBtnClick={e =>
|
||||
itemVisibilityToggleHandler(item, e)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
{!items.length ? (
|
||||
@@ -224,7 +238,7 @@ export default function SavedItemPane({
|
||||
</h2>
|
||||
<img
|
||||
style="max-width: 80%; opacity:0.4"
|
||||
src="assets/empty.svg"
|
||||
src="./assets/empty.svg"
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
133
src/components/Share.jsx
Normal file
133
src/components/Share.jsx
Normal file
@@ -0,0 +1,133 @@
|
||||
import { useEffect, useState } from 'preact/hooks';
|
||||
import { ProBadge } from './ProBadge';
|
||||
import { HStack, Stack, VStack } from './Stack';
|
||||
import Switch from './Switch';
|
||||
import { itemService } from '../itemService';
|
||||
import { alertsService } from '../notifications';
|
||||
import { Button } from './common';
|
||||
import { Icon } from './Icons';
|
||||
import { Text } from './Text';
|
||||
|
||||
const FREE_PUBLIC_ITEM_COUNT = 1;
|
||||
const BASE_URL = location.origin;
|
||||
const TOGGLE_VISIBILITY_API =
|
||||
/*!window.location.origin.includes('localhost')
|
||||
? 'http://127.0.0.1:5001/web-maker-app/us-central1/toggleVisibility'
|
||||
: */ 'https://togglevisibility-ajhkrtmkaq-uc.a.run.app';
|
||||
|
||||
export function Share({
|
||||
user,
|
||||
item,
|
||||
onVisibilityChange,
|
||||
onLoginBtnClick,
|
||||
onProBtnClick
|
||||
}) {
|
||||
const [publicItemCount, setPublicItemCount] = useState();
|
||||
useEffect(() => {
|
||||
if (!user) return;
|
||||
window.db.getPublicItemCount(user.uid).then(c => {
|
||||
setPublicItemCount(c);
|
||||
// console.log('public items', c);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const [val, setVal] = useState(item.isPublic);
|
||||
const onChange = async e => {
|
||||
const newVal = e.target.checked;
|
||||
setVal(newVal);
|
||||
if (newVal) {
|
||||
const token = await window.user.getIdToken();
|
||||
let res;
|
||||
try {
|
||||
res = await fetch(
|
||||
`${TOGGLE_VISIBILITY_API}?token=${token}&itemId=${item.id}`
|
||||
);
|
||||
} catch (e) {
|
||||
alertsService.add('Could not set visiblity to public');
|
||||
setTimeout(() => {
|
||||
setVal(!newVal);
|
||||
}, 400);
|
||||
return;
|
||||
}
|
||||
|
||||
if (res.status >= 200 && res.status < 400) {
|
||||
setPublicItemCount(publicItemCount + 1);
|
||||
onVisibilityChange(true);
|
||||
alertsService.add('Visiblity set to public');
|
||||
} else {
|
||||
alertsService.add('Could not set visiblity to public');
|
||||
setTimeout(() => {
|
||||
setVal(!newVal);
|
||||
}, 400);
|
||||
}
|
||||
} else {
|
||||
itemService.setItem(item.id, { isPublic: false });
|
||||
setPublicItemCount(publicItemCount - 1);
|
||||
onVisibilityChange(false);
|
||||
alertsService.add('Visiblity set to private');
|
||||
}
|
||||
};
|
||||
|
||||
const copyUrl = () => {
|
||||
navigator.clipboard.writeText(`${BASE_URL}/create/${item.id}`);
|
||||
alertsService.add('URL copied to clipboard');
|
||||
};
|
||||
if (!user) {
|
||||
return (
|
||||
<HStack justify="center" gap={2}>
|
||||
<Text>Login to share this creation publicly</Text>
|
||||
<Button class="btn btn--primary" onClick={onLoginBtnClick}>
|
||||
Login
|
||||
</Button>
|
||||
</HStack>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<VStack gap={4} align="stretch">
|
||||
<div style="min-width: 46ch">
|
||||
<VStack gap={1} align="stretch">
|
||||
<Switch
|
||||
checked={val}
|
||||
onChange={onChange}
|
||||
labels={['Private', 'Public']}
|
||||
>
|
||||
Access
|
||||
</Switch>
|
||||
{item.isPublic && (
|
||||
<p>
|
||||
Public at{' '}
|
||||
<a href={`${BASE_URL}/create/${item.id}`} target="_blank">
|
||||
{BASE_URL}/create/{item.id}
|
||||
</a>{' '}
|
||||
<Button
|
||||
class="btn btn--dark hint--bottom hint--rounded"
|
||||
onClick={copyUrl}
|
||||
aria-label="Copy"
|
||||
>
|
||||
<Icon name="copy" />
|
||||
</Button>
|
||||
</p>
|
||||
)}
|
||||
</VStack>
|
||||
</div>
|
||||
|
||||
{!user?.isPro ? (
|
||||
<VStack gap={1} align="stretch">
|
||||
<p>
|
||||
Public creations available: {FREE_PUBLIC_ITEM_COUNT}. Used:{' '}
|
||||
{publicItemCount === undefined ? '-' : publicItemCount}. Left:{' '}
|
||||
{Math.max(0, FREE_PUBLIC_ITEM_COUNT - publicItemCount)}
|
||||
</p>
|
||||
<p>
|
||||
<HStack gap={1}>
|
||||
<span>For unlimited public creations, </span>
|
||||
<button onClick={onProBtnClick} class="btn btn--pro btn--small">
|
||||
Upgrade to Pro
|
||||
</button>
|
||||
</HStack>
|
||||
</p>
|
||||
</VStack>
|
||||
) : null}
|
||||
</VStack>
|
||||
);
|
||||
}
|
54
src/components/Stack.jsx
Normal file
54
src/components/Stack.jsx
Normal file
@@ -0,0 +1,54 @@
|
||||
const gaps = [0, '0.5rem', '1rem', '1.5rem', '3rem', '5rem'];
|
||||
|
||||
const Stack = function ({
|
||||
classes = '',
|
||||
gap = 0,
|
||||
align = 'center',
|
||||
justify = 'flex-start',
|
||||
direction = 'horizontal',
|
||||
fullWidth = false,
|
||||
fullHeight = false,
|
||||
wrap,
|
||||
children
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: gaps[gap] || gap,
|
||||
alignItems: align,
|
||||
justifyContent: justify,
|
||||
flexDirection: direction === 'vertical' ? 'column' : 'row',
|
||||
height: fullHeight ? '100%' : null,
|
||||
width: fullWidth ? '100%' : null,
|
||||
flexWrap: wrap ? 'wrap' : null
|
||||
}}
|
||||
class={`stack ${classes}`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const VStack = props => {
|
||||
return <Stack {...props} direction="vertical" />;
|
||||
};
|
||||
|
||||
const HStack = props => {
|
||||
return (
|
||||
<Stack
|
||||
classes={`hstack ${props.responsive ? 'hstack--responsive' : ''}`}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const Spacer = () => {
|
||||
return (
|
||||
<>
|
||||
<div style={{ flexGrow: '1' }}></div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export { Stack, VStack, HStack, Spacer };
|
@@ -1,5 +1,3 @@
|
||||
import { h } from 'preact';
|
||||
|
||||
export default function Switch({
|
||||
checked,
|
||||
onChange,
|
||||
|
68
src/components/Text.jsx
Normal file
68
src/components/Text.jsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import { forwardRef } from 'preact/compat';
|
||||
|
||||
const appearanceStyles = {
|
||||
normal: {
|
||||
color: 'var(--color-text)'
|
||||
},
|
||||
primary: {
|
||||
color: 'var(--color-heading)'
|
||||
},
|
||||
secondary: {
|
||||
color: 'var(--color-text-light)'
|
||||
},
|
||||
tertiary: {
|
||||
color: 'var(--color-text-lightest-final)'
|
||||
},
|
||||
brand: {
|
||||
color: 'var(--color-brand)'
|
||||
}
|
||||
};
|
||||
|
||||
const sizes = {
|
||||
0: '0.875rem',
|
||||
1: '1rem',
|
||||
2: '1.125rem',
|
||||
3: '1.25rem',
|
||||
4: '1.5rem',
|
||||
5: '2rem',
|
||||
6: '2.5rem',
|
||||
7: '3rem',
|
||||
8: '4rem'
|
||||
};
|
||||
export const Text = forwardRef(
|
||||
(
|
||||
{
|
||||
size = 1,
|
||||
weight = 'normal',
|
||||
tag,
|
||||
style = 'normal',
|
||||
appearance = 'normal',
|
||||
letterSpacing = 0,
|
||||
lineHeight = 1.4,
|
||||
align = 'left',
|
||||
transform,
|
||||
classes = '',
|
||||
children
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const Tag = tag || 'span';
|
||||
|
||||
const styles = {
|
||||
letterSpacing: letterSpacing,
|
||||
fontSize: sizes[size],
|
||||
textTransform: transform,
|
||||
fontWeight: weight,
|
||||
textAlign: align,
|
||||
lineHeight: lineHeight,
|
||||
fontStyle: style === 'italic' ? 'italic' : 'normal',
|
||||
...appearanceStyles[appearance]
|
||||
};
|
||||
|
||||
return (
|
||||
<Tag style={styles} className={classes} ref={ref}>
|
||||
{children}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
);
|
@@ -2,11 +2,12 @@
|
||||
*/
|
||||
|
||||
import { h, Component } from 'preact';
|
||||
import { route } from 'preact-router';
|
||||
// import '../service-worker-registration';
|
||||
import { MainHeader } from './MainHeader.jsx';
|
||||
import ContentWrap from './ContentWrap.jsx';
|
||||
import ContentWrapFiles from './ContentWrapFiles.jsx';
|
||||
import Footer from './Footer.jsx';
|
||||
import { Footer } from './Footer.jsx';
|
||||
import SavedItemPane from './SavedItemPane.jsx';
|
||||
import AddLibrary from './AddLibrary.jsx';
|
||||
import Modal from './Modal.jsx';
|
||||
@@ -68,14 +69,20 @@ import {
|
||||
import { commandPaletteService } from '../commandPaletteService';
|
||||
|
||||
import { I18nProvider } from '@lingui/react';
|
||||
import { Assets } from './Assets.jsx';
|
||||
import { LocalStorageKeys } from '../constants.js';
|
||||
import { Share } from './Share.jsx';
|
||||
import { Pro } from './Pro.jsx';
|
||||
import { VStack } from './Stack.jsx';
|
||||
import { ProBadge } from './ProBadge.jsx';
|
||||
import { Text } from './Text.jsx';
|
||||
|
||||
if (module.hot) {
|
||||
require('preact/debug');
|
||||
}
|
||||
|
||||
const UNSAVED_WARNING_COUNT = 15;
|
||||
const version = '5.3.0';
|
||||
const version = '6.0.0';
|
||||
|
||||
// Read forced settings as query parameters
|
||||
window.forcedSettings = {};
|
||||
@@ -102,6 +109,7 @@ export default class App extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.AUTO_SAVE_INTERVAL = 15000; // 15 seconds
|
||||
const savedUser = window.localStorage.getItem('user');
|
||||
this.modalDefaultStates = {
|
||||
isModalOpen: false,
|
||||
isAddLibraryModalOpen: false,
|
||||
@@ -116,7 +124,11 @@ export default class App extends Component {
|
||||
isOnboardModalOpen: false,
|
||||
isJs13KModalOpen: false,
|
||||
isCreateNewModalOpen: false,
|
||||
isCommandPaletteOpen: false
|
||||
isCommandPaletteOpen: false,
|
||||
isAssetsOpen: false,
|
||||
isShareModalOpen: false,
|
||||
isProModalOpen: false,
|
||||
isFilesLimitModalOpen: false
|
||||
};
|
||||
this.state = {
|
||||
isSavedItemPaneOpen: false,
|
||||
@@ -128,7 +140,8 @@ export default class App extends Component {
|
||||
html: window.codeHtml,
|
||||
css: window.codeCss
|
||||
},
|
||||
catalogs: {}
|
||||
catalogs: {},
|
||||
user: savedUser
|
||||
};
|
||||
this.defaultSettings = {
|
||||
preserveLastCode: true,
|
||||
@@ -161,15 +174,20 @@ export default class App extends Component {
|
||||
};
|
||||
this.prefs = {};
|
||||
|
||||
firebase.auth().onAuthStateChanged(user => {
|
||||
if (savedUser) {
|
||||
window.user = savedUser;
|
||||
}
|
||||
|
||||
firebase.auth().onAuthStateChanged(authUser => {
|
||||
this.setState({ isLoginModalOpen: false });
|
||||
if (user) {
|
||||
log('You are -> ', user);
|
||||
if (authUser) {
|
||||
log('You are -> ', authUser);
|
||||
alertsService.add('You are now logged in!');
|
||||
this.setState({ user: authUser });
|
||||
window.user = authUser;
|
||||
window.localStorage.setItem('user', authUser);
|
||||
trackEvent('fn', 'loggedIn', window.IS_EXTENSION ? 'extension' : 'web');
|
||||
|
||||
this.setState({ user });
|
||||
window.user = user;
|
||||
if (!window.localStorage[LocalStorageKeys.ASKED_TO_IMPORT_CREATIONS]) {
|
||||
this.fetchItems(false, true).then(items => {
|
||||
if (!items.length) {
|
||||
@@ -183,11 +201,16 @@ export default class App extends Component {
|
||||
trackEvent('ui', 'askToImportModalSeen');
|
||||
});
|
||||
}
|
||||
window.db.getUser(user.uid).then(customUser => {
|
||||
// storing actual firebase user object for accessing functions like updateProfile
|
||||
// window.user.firebaseUser = authUser
|
||||
|
||||
window.db.getUser(authUser.uid).then(customUser => {
|
||||
if (customUser) {
|
||||
const prefs = { ...this.state.prefs };
|
||||
Object.assign(prefs, user.settings);
|
||||
this.setState({ prefs }, this.updateSetting);
|
||||
Object.assign(prefs, authUser.settings);
|
||||
const newUser = { ...authUser, isPro: false, ...customUser };
|
||||
window.localStorage.setItem('user', newUser);
|
||||
this.setState({ user: newUser, prefs }, this.updateSetting);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
@@ -236,6 +259,18 @@ export default class App extends Component {
|
||||
this.setCurrentItem(this.state.currentItem).then(() => {
|
||||
this.refreshEditor();
|
||||
});
|
||||
} else if (this.props.itemId) {
|
||||
window.db
|
||||
.fetchItem(this.props.itemId)
|
||||
.then(item => {
|
||||
this.setCurrentItem(item).then(() => this.refreshEditor());
|
||||
})
|
||||
.catch(err => {
|
||||
alert('No such creation found!');
|
||||
this.createNewItem();
|
||||
|
||||
// route('/');
|
||||
});
|
||||
} else if (result.preserveLastCode && lastCode) {
|
||||
this.setState({ unsavedEditCount: 0 });
|
||||
log('Load last unsaved item', lastCode);
|
||||
@@ -348,9 +383,11 @@ export default class App extends Component {
|
||||
}
|
||||
const fork = JSON.parse(JSON.stringify(sourceItem));
|
||||
delete fork.id;
|
||||
delete fork.createdBy;
|
||||
fork.title = '(Forked) ' + sourceItem.title;
|
||||
fork.updatedOn = Date.now();
|
||||
this.setCurrentItem(fork).then(() => this.refreshEditor());
|
||||
route('/create');
|
||||
alertsService.add(`"${sourceItem.title}" was forked`);
|
||||
trackEvent('fn', 'itemForked');
|
||||
}
|
||||
@@ -410,10 +447,12 @@ export default class App extends Component {
|
||||
};
|
||||
}
|
||||
this.setCurrentItem(item).then(() => this.refreshEditor());
|
||||
route('/create');
|
||||
alertsService.add('New item created');
|
||||
}
|
||||
openItem(item) {
|
||||
this.setCurrentItem(item).then(() => this.refreshEditor());
|
||||
route(`/create/${item.id}`);
|
||||
alertsService.add('Saved item loaded');
|
||||
}
|
||||
removeItem(item) {
|
||||
@@ -540,6 +579,9 @@ export default class App extends Component {
|
||||
openAddLibrary() {
|
||||
this.setState({ isAddLibraryModalOpen: true });
|
||||
}
|
||||
assetsBtnClickHandler() {
|
||||
this.setState({ isAssetsOpen: true });
|
||||
}
|
||||
closeSavedItemsPane() {
|
||||
this.setState({
|
||||
isSavedItemPaneOpen: false
|
||||
@@ -558,6 +600,7 @@ export default class App extends Component {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
console.log('itemId', this.props.itemId);
|
||||
function setBodySize() {
|
||||
document.body.style.height = `${window.innerHeight}px`;
|
||||
}
|
||||
@@ -895,6 +938,15 @@ export default class App extends Component {
|
||||
var isNewItem = !this.state.currentItem.id;
|
||||
this.state.currentItem.id =
|
||||
this.state.currentItem.id || 'item-' + generateRandomId();
|
||||
if (
|
||||
this.state.currentItem.createdBy &&
|
||||
this.state.currentItem.createdBy !== this.state.user.uid
|
||||
) {
|
||||
alertsService.add(
|
||||
'You cannot save this item as it was created by someone else. Fork it to save it as your own.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
isSaving: true
|
||||
});
|
||||
@@ -1047,6 +1099,7 @@ export default class App extends Component {
|
||||
trackEvent('fn', 'loggedOut');
|
||||
auth.logout();
|
||||
this.setState({ isProfileModalOpen: false });
|
||||
this.createNewItem();
|
||||
alertsService.add('Log out successfull');
|
||||
}
|
||||
|
||||
@@ -1089,6 +1142,14 @@ export default class App extends Component {
|
||||
trackEvent('ui', 'openBtnClick');
|
||||
this.openSavedItemsPane();
|
||||
}
|
||||
shareBtnClickHandler() {
|
||||
trackEvent('ui', 'shareBtnClick');
|
||||
if (!window.user || this.state.currentItem.id) {
|
||||
this.setState({ isShareModalOpen: true });
|
||||
} else {
|
||||
alertsService.add('Please save your creation before sharing.');
|
||||
}
|
||||
}
|
||||
detachedPreviewBtnHandler() {
|
||||
trackEvent('ui', 'detachPreviewBtnClick');
|
||||
|
||||
@@ -1105,6 +1166,15 @@ export default class App extends Component {
|
||||
trackEvent('ui', 'notificationButtonClick', version);
|
||||
return false;
|
||||
}
|
||||
proBtnClickHandler() {
|
||||
if (this.state.user?.isPro) {
|
||||
this.setState({ isProfileModalOpen: true });
|
||||
trackEvent('ui', 'manageProBtnClick');
|
||||
} else {
|
||||
this.setState({ isProModalOpen: true });
|
||||
trackEvent('ui', 'proBtnClick');
|
||||
}
|
||||
}
|
||||
codepenBtnClickHandler(e) {
|
||||
if (this.state.currentItem.cssMode === CssModes.ACSS) {
|
||||
alert(
|
||||
@@ -1445,9 +1515,11 @@ export default class App extends Component {
|
||||
this.setState({ isCreateNewModalOpen: false });
|
||||
} else {
|
||||
trackEvent('ui', 'FileModeCreationLimitMessageSeen');
|
||||
return alert(
|
||||
'"Files mode" is currently in beta and is limited to only 2 creations per user. You have already made 2 creations in Files mode.\n\nNote: You can choose to delete old ones to create new.'
|
||||
);
|
||||
// this.closeAllOverlays();
|
||||
this.setState({ isFilesLimitModalOpen: true });
|
||||
// return alert(
|
||||
// '"Files mode" is currently in beta and is limited to only 2 creations per user. You have already made 2 creations in Files mode.\n\nNote: You can choose to delete old ones to create new.'
|
||||
// );
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1623,10 +1695,12 @@ export default class App extends Component {
|
||||
loginBtnHandler={this.loginBtnClickHandler.bind(this)}
|
||||
profileBtnHandler={this.profileBtnClickHandler.bind(this)}
|
||||
addLibraryBtnHandler={this.openAddLibrary.bind(this)}
|
||||
assetsBtnHandler={this.assetsBtnClickHandler.bind(this)}
|
||||
shareBtnHandler={this.shareBtnClickHandler.bind(this)}
|
||||
runBtnClickHandler={this.runBtnClickHandler.bind(this)}
|
||||
isFetchingItems={this.state.isFetchingItems}
|
||||
isSaving={this.state.isSaving}
|
||||
title={this.state.currentItem.title}
|
||||
currentItem={this.state.currentItem}
|
||||
titleInputBlurHandler={this.titleInputBlurHandler.bind(this)}
|
||||
user={this.state.user}
|
||||
isAutoPreviewOn={this.state.prefs.autoPreview}
|
||||
@@ -1634,6 +1708,9 @@ export default class App extends Component {
|
||||
isFileMode={
|
||||
this.state.currentItem && this.state.currentItem.files
|
||||
}
|
||||
onItemFork={() => {
|
||||
this.forkItem(this.state.currentItem);
|
||||
}}
|
||||
/>
|
||||
{this.state.currentItem && this.state.currentItem.files ? (
|
||||
<ContentWrapFiles
|
||||
@@ -1669,6 +1746,7 @@ export default class App extends Component {
|
||||
|
||||
<Footer
|
||||
prefs={this.state.prefs}
|
||||
user={this.state.user}
|
||||
layoutBtnClickHandler={this.layoutBtnClickHandler.bind(this)}
|
||||
helpBtnClickHandler={() =>
|
||||
this.setState({ isHelpModalOpen: true })
|
||||
@@ -1695,6 +1773,7 @@ export default class App extends Component {
|
||||
onJs13KDownloadBtnClick={this.js13KDownloadBtnClickHandler.bind(
|
||||
this
|
||||
)}
|
||||
proBtnClickHandler={this.proBtnClickHandler.bind(this)}
|
||||
hasUnseenChangelog={this.state.hasUnseenChangelog}
|
||||
codeSize={this.state.codeSize}
|
||||
/>
|
||||
@@ -1731,6 +1810,7 @@ export default class App extends Component {
|
||||
onChange={this.onExternalLibChange.bind(this)}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
show={this.state.isNotificationsModalOpen}
|
||||
closeHandler={() =>
|
||||
@@ -1751,13 +1831,7 @@ export default class App extends Component {
|
||||
onChange={this.updateSetting.bind(this)}
|
||||
/>
|
||||
</Modal>
|
||||
<Modal
|
||||
extraClasses="login-modal"
|
||||
show={this.state.isLoginModalOpen}
|
||||
closeHandler={() => this.setState({ isLoginModalOpen: false })}
|
||||
>
|
||||
<Login />
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
show={this.state.isProfileModalOpen}
|
||||
closeHandler={() => this.setState({ isProfileModalOpen: false })}
|
||||
@@ -1767,6 +1841,70 @@ export default class App extends Component {
|
||||
logoutBtnHandler={this.logout.bind(this)}
|
||||
/>
|
||||
</Modal>
|
||||
<Modal
|
||||
show={this.state.isAssetsOpen}
|
||||
closeHandler={() => this.setState({ isAssetsOpen: false })}
|
||||
>
|
||||
<Assets
|
||||
onProBtnClick={() => {
|
||||
this.setState({ isAssetsOpen: false });
|
||||
this.proBtnClickHandler();
|
||||
}}
|
||||
onLoginBtnClick={() => {
|
||||
this.closeAllOverlays();
|
||||
this.loginBtnClickHandler();
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
<Modal
|
||||
show={this.state.isShareModalOpen}
|
||||
closeHandler={() => this.setState({ isShareModalOpen: false })}
|
||||
>
|
||||
<Share
|
||||
user={this.state.user}
|
||||
item={this.state.currentItem}
|
||||
onVisibilityChange={visibility => {
|
||||
const item = {
|
||||
...this.state.currentItem,
|
||||
isPublic: visibility
|
||||
};
|
||||
this.setState({ currentItem: item });
|
||||
}}
|
||||
onLoginBtnClick={() => {
|
||||
this.closeAllOverlays();
|
||||
this.loginBtnClickHandler();
|
||||
}}
|
||||
onProBtnClick={() => {
|
||||
this.closeAllOverlays();
|
||||
this.proBtnClickHandler();
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
<Modal
|
||||
show={this.state.isProModalOpen}
|
||||
closeHandler={() => this.setState({ isProModalOpen: false })}
|
||||
extraClasses="pro-modal"
|
||||
>
|
||||
<Pro
|
||||
user={this.state.user}
|
||||
onLoginClick={() => {
|
||||
this.closeAllOverlays();
|
||||
this.loginBtnClickHandler();
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
{/* Login modal is intentionally kept here after assets & share modal because
|
||||
they trigger this modal and if order isn't maintainer, the modal overlay doesn't
|
||||
show properly */}
|
||||
<Modal
|
||||
extraClasses="login-modal"
|
||||
show={this.state.isLoginModalOpen}
|
||||
closeHandler={() => this.setState({ isLoginModalOpen: false })}
|
||||
>
|
||||
<Login />
|
||||
</Modal>
|
||||
|
||||
<HelpModal
|
||||
show={this.state.isHelpModalOpen}
|
||||
closeHandler={() => this.setState({ isHelpModalOpen: false })}
|
||||
@@ -1820,6 +1958,23 @@ export default class App extends Component {
|
||||
)}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
extraClasses=""
|
||||
show={this.state.isFilesLimitModalOpen}
|
||||
closeHandler={() => this.setState({ isFilesLimitModalOpen: false })}
|
||||
>
|
||||
<VStack align="stretch" gap={2}>
|
||||
<Text tag="p">
|
||||
You have used your quota of 2 'Files mode' creations in Free
|
||||
plan.
|
||||
</Text>
|
||||
<Text tag="p">
|
||||
You can choose to delete old ones to free quota or upgrade to{' '}
|
||||
<ProBadge />.{' '}
|
||||
</Text>
|
||||
</VStack>
|
||||
</Modal>
|
||||
|
||||
<CommandPalette
|
||||
show={this.state.isCommandPaletteOpen}
|
||||
files={linearizeFiles(this.state.currentItem.files || [])}
|
||||
|
@@ -4,11 +4,15 @@ import { trackEvent } from '../analytics';
|
||||
class Clickable extends Component {
|
||||
handleClick(e) {
|
||||
const el = e.currentTarget;
|
||||
trackEvent(
|
||||
el.getAttribute('data-event-category'),
|
||||
el.getAttribute('data-event-action')
|
||||
);
|
||||
this.props.onClick(e);
|
||||
if (el.getAttribute('data-event-category')) {
|
||||
trackEvent(
|
||||
el.getAttribute('data-event-category'),
|
||||
el.getAttribute('data-event-action')
|
||||
);
|
||||
}
|
||||
if (this.props.onClick) {
|
||||
this.props.onClick(e);
|
||||
}
|
||||
}
|
||||
render() {
|
||||
/* eslint-disable no-unused-vars */
|
||||
@@ -38,5 +42,5 @@ export function Divider(props) {
|
||||
}
|
||||
|
||||
export function BetaTag() {
|
||||
return <span class="beta-tag">Beta</span>;
|
||||
return <span class="badge beta-tag">Beta</span>;
|
||||
}
|
||||
|
78
src/db.js
78
src/db.js
@@ -5,6 +5,24 @@ import { deferred } from './deferred';
|
||||
import { trackEvent } from './analytics';
|
||||
import { log } from './utils';
|
||||
|
||||
/**
|
||||
* Converts a firestore query snapshot into native array
|
||||
* @param {snapshot} querySnapshot Snapshot object returned by a firestore query
|
||||
*/
|
||||
function getArrayFromQuerySnapshot(querySnapshot) {
|
||||
const arr = [];
|
||||
querySnapshot.forEach(doc => {
|
||||
// doc.data() has to be after doc.id because docs can have `id` key in them which
|
||||
// should override the explicit `id` being set
|
||||
arr.push({
|
||||
id: doc.id,
|
||||
...doc.data()
|
||||
});
|
||||
// documentCache[doc.id] = doc.data()
|
||||
});
|
||||
return arr;
|
||||
}
|
||||
|
||||
(() => {
|
||||
const FAUX_DELAY = 1;
|
||||
|
||||
@@ -59,7 +77,7 @@ import { log } from './utils';
|
||||
|
||||
return firestoreInstance
|
||||
.enablePersistence({ experimentalTabSynchronization: true })
|
||||
.then(function() {
|
||||
.then(function () {
|
||||
// Initialize Cloud Firestore through firebase
|
||||
db = firebase.firestore();
|
||||
// const settings = {
|
||||
@@ -69,7 +87,7 @@ import { log } from './utils';
|
||||
log('firebase db ready', db);
|
||||
resolve(db);
|
||||
})
|
||||
.catch(function(err) {
|
||||
.catch(function (err) {
|
||||
reject(err.code);
|
||||
if (err.code === 'failed-precondition') {
|
||||
// Multiple tabs open, persistence can only be enabled
|
||||
@@ -113,7 +131,7 @@ import { log } from './utils';
|
||||
{
|
||||
lastSeenVersion: version
|
||||
},
|
||||
function() {}
|
||||
function () {}
|
||||
);
|
||||
if (window.user) {
|
||||
const remoteDb = await getDb();
|
||||
@@ -129,19 +147,33 @@ import { log } from './utils';
|
||||
.doc(`users/${userId}`)
|
||||
.get()
|
||||
.then(doc => {
|
||||
if (!doc.exists)
|
||||
return remoteDb.doc(`users/${userId}`).set(
|
||||
{},
|
||||
{
|
||||
merge: true
|
||||
}
|
||||
);
|
||||
if (!doc.exists) {
|
||||
// return remoteDb.doc(`users/${userId}`).set(
|
||||
// {},
|
||||
// {
|
||||
// merge: true
|
||||
// }
|
||||
// );
|
||||
return {};
|
||||
}
|
||||
const user = doc.data();
|
||||
Object.assign(window.user, user);
|
||||
window.user = { ...window.user, ...user };
|
||||
return user;
|
||||
});
|
||||
}
|
||||
|
||||
async function fetchItem(itemId) {
|
||||
const remoteDb = await getDb();
|
||||
return remoteDb
|
||||
.doc(`items/${itemId}`)
|
||||
.get()
|
||||
.then(doc => {
|
||||
if (!doc.exists) return {};
|
||||
const data = doc.data();
|
||||
return data;
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch user settings.
|
||||
// This isn't hitting the remote db because remote settings
|
||||
// get fetch asynchronously (in user/) and update the envioronment.
|
||||
@@ -155,12 +187,36 @@ import { log } from './utils';
|
||||
return d.promise;
|
||||
}
|
||||
|
||||
async function getPublicItemCount(userId) {
|
||||
const remoteDb = await getDb();
|
||||
return remoteDb
|
||||
.collection('items')
|
||||
.where('createdBy', '==', userId)
|
||||
.where('isPublic', '==', true)
|
||||
.get()
|
||||
.then(snapShot => {
|
||||
return snapShot.size;
|
||||
});
|
||||
}
|
||||
|
||||
async function getUserSubscriptionEvents(userId) {
|
||||
const remoteDb = await getDb();
|
||||
return remoteDb
|
||||
.collection('subscriptions')
|
||||
.where('userId', '==', userId)
|
||||
.get()
|
||||
.then(getArrayFromQuerySnapshot);
|
||||
}
|
||||
|
||||
window.db = {
|
||||
getDb,
|
||||
getUser,
|
||||
getUserLastSeenVersion,
|
||||
setUserLastSeenVersion,
|
||||
getSettings,
|
||||
fetchItem,
|
||||
getPublicItemCount,
|
||||
getUserSubscriptionEvents,
|
||||
local: dbLocalAlias,
|
||||
sync: dbSyncAlias
|
||||
};
|
||||
|
20
src/hooks/useCheckout.js
Normal file
20
src/hooks/useCheckout.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
function useCheckout() {
|
||||
const [hasVendorScriptLoaded, setHasVendorScriptLoaded] = useState();
|
||||
useEffect(() => {
|
||||
const script = document.createElement('script');
|
||||
script.src = 'https://app.lemonsqueezy.com/js/lemon.js';
|
||||
script.async = 'true';
|
||||
script.defer = 'true';
|
||||
script.addEventListener('load', () => {
|
||||
window.createLemonSqueezy();
|
||||
setHasVendorScriptLoaded(true);
|
||||
});
|
||||
document.body.appendChild(script);
|
||||
}, []);
|
||||
|
||||
return hasVendorScriptLoaded;
|
||||
}
|
||||
|
||||
export { useCheckout };
|
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
@@ -11,11 +11,12 @@
|
||||
rel="manifest"
|
||||
href="<%= htmlWebpackPlugin.files.publicPath %>manifest.json"
|
||||
/>
|
||||
<% if (htmlWebpackPlugin.options.manifest.theme_color) { %>
|
||||
<meta
|
||||
name="theme-color"
|
||||
content="<%= htmlWebpackPlugin.options.manifest.theme_color %>"
|
||||
/>
|
||||
<% if (cli.env.isProd) { %>
|
||||
<base href="/create/" />
|
||||
<% } else { %>
|
||||
<base href="/" />
|
||||
<% } %> <% if (cli.manifest.theme_color) { %>
|
||||
<meta name="theme-color" content="<%= cli.manifest.theme_color %>" />
|
||||
<% } %>
|
||||
|
||||
<style>
|
||||
@@ -38,7 +39,7 @@
|
||||
<link
|
||||
rel="stylesheet"
|
||||
id="editorThemeLinkTag"
|
||||
href="lib/codemirror/theme/monokai.css"
|
||||
href="./lib/codemirror/theme/monokai.css"
|
||||
/>
|
||||
|
||||
<style id="fontStyleTemplate" type="template">
|
||||
@@ -73,13 +74,13 @@
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<div id="portal"></div>
|
||||
<!-- SCRIPT-TAGS -->
|
||||
<%= htmlWebpackPlugin.options.ssr({ url: '/' }) %>
|
||||
<script
|
||||
defer
|
||||
src="<%= htmlWebpackPlugin.files.chunks['bundle'].entry %>"
|
||||
></script>
|
||||
|
||||
<!-- END-SCRIPT-TAGS -->
|
||||
<%= cli.ssr %> <% if (cli.config.prerender === true) { %>
|
||||
<script type="__PREACT_CLI_DATA__">
|
||||
<%= encodeURI(JSON.stringify(cli.CLI_DATA)) %>
|
||||
</script>
|
||||
<% } %>
|
||||
<script type="module" src="<%= cli.entrypoints['bundle'] %>"></script>
|
||||
<script nomodule src="<%= cli.entrypoints['dom-polyfills'] %>"></script>
|
||||
</body>
|
||||
</html>
|
12
src/index.js
12
src/index.js
@@ -1,3 +1,4 @@
|
||||
import Router from 'preact-router';
|
||||
import App from './components/app.jsx';
|
||||
|
||||
import './lib/codemirror/lib/codemirror.css';
|
||||
@@ -8,4 +9,13 @@ import './lib/hint.min.css';
|
||||
import './lib/inlet.css';
|
||||
import './style.css';
|
||||
|
||||
export default App;
|
||||
export default function () {
|
||||
return (
|
||||
<Router>
|
||||
<App path="/" />
|
||||
<App path="/create/:itemId" />
|
||||
<App path="/app/create/:itemId" />
|
||||
<App default />
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
|
12
src/indexpm.html
Normal file
12
src/indexpm.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<script>
|
||||
function callback(e) {
|
||||
// console.log('post message recvd', e.data);
|
||||
window.document.open();
|
||||
|
||||
const { contents } = e.data;
|
||||
window.document.write(e.data.contents);
|
||||
window.document.close();
|
||||
window.addEventListener('message', callback);
|
||||
}
|
||||
window.addEventListener('message', callback);
|
||||
</script>
|
@@ -1,5 +1,5 @@
|
||||
import { deferred } from './deferred';
|
||||
import { log } from 'util';
|
||||
import { log } from './utils';
|
||||
import firebase from 'firebase/app';
|
||||
|
||||
export const itemService = {
|
||||
@@ -52,15 +52,15 @@ export const itemService = {
|
||||
.collection('items')
|
||||
.where('createdBy', '==', window.user.uid)
|
||||
.onSnapshot(
|
||||
function(querySnapshot) {
|
||||
querySnapshot.forEach(function(doc) {
|
||||
function (querySnapshot) {
|
||||
querySnapshot.forEach(function (doc) {
|
||||
items.push(doc.data());
|
||||
});
|
||||
log('Items fetched in ', Date.now() - t, 'ms');
|
||||
|
||||
d.resolve(items);
|
||||
},
|
||||
function() {
|
||||
function () {
|
||||
d.resolve([]);
|
||||
}
|
||||
);
|
||||
@@ -147,7 +147,7 @@ export const itemService = {
|
||||
{
|
||||
items: {}
|
||||
},
|
||||
function(result) {
|
||||
function (result) {
|
||||
/* eslint-disable guard-for-in */
|
||||
for (var id in items) {
|
||||
result.items[id] = true;
|
||||
@@ -205,7 +205,7 @@ export const itemService = {
|
||||
{
|
||||
items: {}
|
||||
},
|
||||
function(result) {
|
||||
function (result) {
|
||||
result.items[itemId] = true;
|
||||
window.db.local.set({
|
||||
items: result.items
|
||||
@@ -235,7 +235,7 @@ export const itemService = {
|
||||
{
|
||||
items: {}
|
||||
},
|
||||
function(result) {
|
||||
function (result) {
|
||||
delete result.items[itemId];
|
||||
window.db.local.set({
|
||||
items: result.items
|
||||
|
30
src/lib/hint.min.css
vendored
30
src/lib/hint.min.css
vendored
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Settings - Web Maker</title>
|
||||
|
491
src/style.css
491
src/style.css
@@ -3,16 +3,33 @@
|
||||
--color-text-dark-1: #b3aec4;
|
||||
--color-bg: #252637;
|
||||
--color-popup: #3a2b63;
|
||||
--color-overlay: rgb(0 0 0 / 40%);
|
||||
--color-close-btn: #d12b4a;
|
||||
--code-font-size: 16px;
|
||||
--color-button: #d3a447;
|
||||
--color-button: #e3ba26;
|
||||
--color-focus-outline: #d3a447;
|
||||
--color-form-control-bg: #2c214b;
|
||||
|
||||
--color-heading: #fff;
|
||||
--color-text-light: #b0a5d6;
|
||||
--color-text-lightest-final: #787090;
|
||||
--clr-brand: purple;
|
||||
|
||||
--color-pro-1: #1fffb3;
|
||||
--color-pro-2: #f2ff00;
|
||||
|
||||
--color-btn-hover-1: hsl(53.35deg 100% 50%);
|
||||
--color-btn-hover-2: hsl(38.96deg 100% 50%);
|
||||
|
||||
--footer-height: 37px;
|
||||
--console-height: 32px;
|
||||
|
||||
--duration-modal-show: 0.3s;
|
||||
--duration-modal-overlay-show: 0.2s;
|
||||
|
||||
--zindex-modal-overlay: 5;
|
||||
--zindex-footer: 6;
|
||||
--zindex-modal: 2000;
|
||||
}
|
||||
|
||||
html {
|
||||
@@ -85,6 +102,7 @@ p {
|
||||
button {
|
||||
font-family: inherit;
|
||||
font-size: 100%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.hide {
|
||||
@@ -120,11 +138,13 @@ button {
|
||||
.ai-c {
|
||||
align-items: center;
|
||||
}
|
||||
.flex-h-center {
|
||||
.flex-h-center,
|
||||
.jc-c {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.flex-h-end {
|
||||
.flex-h-end,
|
||||
.jc-fe {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
@@ -256,11 +276,17 @@ label {
|
||||
width: 1px;
|
||||
height: 100%;
|
||||
}
|
||||
[class*='hint--']:after,
|
||||
[class*='hint--']:before {
|
||||
background-color: #000;
|
||||
border-radius: 0 4px 0 0;
|
||||
}
|
||||
[class*='hint--']:after {
|
||||
text-transform: none;
|
||||
font-weight: normal;
|
||||
letter-spacing: 0.5px;
|
||||
font-size: 14px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.line {
|
||||
@@ -373,6 +399,7 @@ a > svg {
|
||||
float: right;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-inline-start: 2rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
@@ -382,7 +409,7 @@ a > svg {
|
||||
font-size: inherit;
|
||||
background: transparent;
|
||||
border: 3px solid var(--color-button);
|
||||
border-radius: 5px;
|
||||
border-radius: 2rem;
|
||||
padding: 9px 15px;
|
||||
cursor: pointer;
|
||||
letter-spacing: 0.2px;
|
||||
@@ -394,10 +421,17 @@ a > svg {
|
||||
}
|
||||
|
||||
.btn--primary {
|
||||
background: var(--color-button)
|
||||
linear-gradient(180deg, rgba(0, 0, 0, 0.15) 0px, transparent);
|
||||
--black-mix: 70%;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
var(--color-button),
|
||||
color-mix(in lch, var(--color-button), black)
|
||||
);
|
||||
color: black;
|
||||
font-weight: 600;
|
||||
border: 1px solid
|
||||
color-mix(in lch, var(--color-button), black var(--black-mix));
|
||||
box-shadow: inset 0 1px 0px 0 rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.btn--big {
|
||||
@@ -410,11 +444,26 @@ a > svg {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
text-decoration: none;
|
||||
box-shadow: 0 5px 5px 0 rgba(0, 0, 0, 0.25);
|
||||
.btn--small {
|
||||
padding: 0.2rem 0.5rem;
|
||||
text-transform: uppercase;
|
||||
/* border-radius: 3px; */
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
--black-mix: 90%;
|
||||
text-decoration: none;
|
||||
box-shadow:
|
||||
rgba(0, 0, 0, 0.1) 0px 20px 25px -5px,
|
||||
rgba(0, 0, 0, 0.04) 0px 10px 10px -5px;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
*:focus {
|
||||
outline-width: 3px;
|
||||
outline-color: var(--color-button);
|
||||
@@ -436,6 +485,31 @@ a > svg {
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
@property --angle {
|
||||
syntax: '<angle>';
|
||||
initial-value: 0deg;
|
||||
inherits: false;
|
||||
}
|
||||
.btn--pro {
|
||||
background: linear-gradient(
|
||||
var(--angle),
|
||||
var(--color-pro-1),
|
||||
var(--color-pro-2)
|
||||
);
|
||||
color: #222;
|
||||
border: 0;
|
||||
font-weight: 700;
|
||||
animation: gradient_move 3s linear infinite;
|
||||
}
|
||||
@keyframes gradient_move {
|
||||
from {
|
||||
--angle: 0deg;
|
||||
}
|
||||
to {
|
||||
--angle: 360deg;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-loader {
|
||||
display: none;
|
||||
}
|
||||
@@ -479,8 +553,6 @@ a > svg {
|
||||
body:not(.light-version).overlay-visible .main-container {
|
||||
transition-duration: 0.5s;
|
||||
transform: scale(0.98);
|
||||
/* transition-delay: 0.4s; */
|
||||
filter: blur(2px);
|
||||
}
|
||||
|
||||
.content-wrap {
|
||||
@@ -762,7 +834,7 @@ body > #demo-frame {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
/* Because .console is 6 */
|
||||
z-index: 6;
|
||||
z-index: var(--zindex-footer);
|
||||
}
|
||||
|
||||
.main-header {
|
||||
@@ -774,6 +846,8 @@ body > #demo-frame {
|
||||
|
||||
.btn--dark,
|
||||
.main-header__btn-wrap > button {
|
||||
--clr-1: hsl(0, 0%, 25%);
|
||||
--clr-2: hsl(0, 0%, 13%);
|
||||
box-sizing: content-box;
|
||||
/* text-transform: uppercase; */
|
||||
/* font-size: 0.875rem; */
|
||||
@@ -787,7 +861,8 @@ body > #demo-frame {
|
||||
margin-left: 10px;
|
||||
padding: 3px 8px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.9);
|
||||
background: linear-gradient(180deg, hsl(0, 0%, 25%) 0, hsl(0, 0%, 13%) 100%);
|
||||
border-radius: 5px;
|
||||
background: linear-gradient(180deg, var(--clr-1) 0, var(--clr-2) 100%);
|
||||
box-shadow: inset 0 1px 0px 0 rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
@@ -805,13 +880,20 @@ body > #demo-frame {
|
||||
}
|
||||
|
||||
.btn--dark:hover {
|
||||
background: #9297b3;
|
||||
/* --clr-1: #6844ad; */
|
||||
|
||||
--clr-1: hsl(53.35deg 100% 50%);
|
||||
--clr-2: hsl(38.96deg 100% 50%);
|
||||
color: #111;
|
||||
/* border-color: rgba(146, 151, 179, 0.5); */
|
||||
}
|
||||
.btn--dark:hover > svg {
|
||||
fill: #111;
|
||||
}
|
||||
.btn--dark.btn--active {
|
||||
background: linear-gradient(0deg, hsl(0, 0%, 25%) 0, hsl(0, 0%, 13%) 100%);
|
||||
box-shadow: inset 0 -1px 0px 0 rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
.btn--chromeless {
|
||||
box-shadow: none;
|
||||
background: transparent;
|
||||
@@ -938,6 +1020,29 @@ body > #demo-frame {
|
||||
/* border-radius: 4px; */
|
||||
}
|
||||
|
||||
.dialog__close-btn {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
border: none;
|
||||
background: var(--color-close-btn);
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
var(--color-close-btn),
|
||||
color-mix(in lch, var(--color-close-btn), black)
|
||||
);
|
||||
color: white;
|
||||
border-radius: 0.3rem;
|
||||
padding: 0.4rem 0.5rem;
|
||||
}
|
||||
.dialog__close-btn > svg {
|
||||
width: 1.2rem;
|
||||
aspect-ratio: 1;
|
||||
}
|
||||
.dialog__close-btn:hover {
|
||||
--color-close-btn: var(--color-btn-hover-1);
|
||||
color: #111;
|
||||
}
|
||||
|
||||
.modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
@@ -949,13 +1054,23 @@ body > #demo-frame {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: center;
|
||||
z-index: 2000;
|
||||
z-index: var(--zindex-modal);
|
||||
visibility: hidden;
|
||||
/* background-color: rgba(102, 51, 153, 0.7); */
|
||||
/* background-color: rgba(0, 0, 0, 0.7); */
|
||||
/* To prevent scroll repaint inside modal */
|
||||
|
||||
z-index: var(--zindex-modal-overlay);
|
||||
/* opacity: 0; */
|
||||
will-change: opacity;
|
||||
background: var(--color-overlay);
|
||||
backdrop-filter: blur(5px) grayscale(1);
|
||||
transition: opacity var(--duration-modal-overlay-show);
|
||||
|
||||
will-change: transform;
|
||||
}
|
||||
.modal--no-overlay {
|
||||
background: none;
|
||||
backdrop-filter: none;
|
||||
}
|
||||
|
||||
@keyframes anim-modal-overlay {
|
||||
to {
|
||||
@@ -964,32 +1079,28 @@ body > #demo-frame {
|
||||
}
|
||||
}
|
||||
|
||||
.modal__close-btn {
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
top: 1rem;
|
||||
text-transform: uppercase;
|
||||
font-weight: 700;
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.8;
|
||||
transition: 0.25s ease;
|
||||
border: 1px solid black;
|
||||
border-radius: 2px;
|
||||
padding: 0.2rem 0.5rem;
|
||||
}
|
||||
|
||||
.modal__close-btn > svg {
|
||||
fill: black;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.modal__close-btn:hover {
|
||||
opacity: 0.7;
|
||||
--color-close-btn: var(--color-btn-hover-1);
|
||||
color: #111;
|
||||
}
|
||||
.modal__close-btn:hover > svg {
|
||||
fill: #111;
|
||||
}
|
||||
|
||||
.modal__close-btn {
|
||||
right: 0rem;
|
||||
bottom: calc(100% + 0.2rem);
|
||||
transition: 0.25s ease;
|
||||
}
|
||||
.modal__content {
|
||||
background: var(--color-popup);
|
||||
|
||||
/* fix me */
|
||||
background: linear-gradient(45deg, #2d063cad, #3a2b63);
|
||||
box-shadow:
|
||||
inset 1px -1px 0 0 #ffffff17,
|
||||
0 20px 31px 0 #0000008a;
|
||||
|
||||
color: var(--color-text);
|
||||
position: relative;
|
||||
border-radius: 5px;
|
||||
@@ -998,9 +1109,9 @@ body > #demo-frame {
|
||||
font-size: 1.1em;
|
||||
line-height: 1.4;
|
||||
max-width: 85vw;
|
||||
margin: 2rem auto;
|
||||
margin: 4rem auto 2rem;
|
||||
box-sizing: border-box;
|
||||
overflow-y: auto;
|
||||
/* overflow-y: auto; */
|
||||
pointer-events: auto;
|
||||
transform: scale(0.98);
|
||||
animation: anim-modal var(--duration-modal-show) cubic-bezier(0.4, 0, 0.2, 1)
|
||||
@@ -1029,6 +1140,7 @@ body > #demo-frame {
|
||||
/* transition-duration: 0.3s; */
|
||||
/* transform: translateY(0px) scale(1); */
|
||||
/* opacity: 1; */
|
||||
backdrop-filter: blur(3px);
|
||||
}
|
||||
|
||||
.modal-overlay {
|
||||
@@ -1038,10 +1150,11 @@ body > #demo-frame {
|
||||
visibility: hidden;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 5;
|
||||
z-index: var(--zindex-modal-overlay);
|
||||
opacity: 0;
|
||||
will-change: opacity;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
/* background: var(--color-overlay); */
|
||||
backdrop-filter: blur(5px) grayscale(1);
|
||||
transition: opacity var(--duration-modal-overlay-show);
|
||||
}
|
||||
|
||||
@@ -1071,11 +1184,13 @@ body > #demo-frame {
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 450px;
|
||||
width: 35vw;
|
||||
min-width: 40ch;
|
||||
max-width: 60ch;
|
||||
padding: 20px 30px;
|
||||
z-index: 6;
|
||||
visibility: hidden; /* prevents tabbing */
|
||||
background-color: var(--color-popup);
|
||||
background: var(--color-popup);
|
||||
transition: 0.3s cubic-bezier(1, 0.13, 0.21, 0.87);
|
||||
transition-property: transform;
|
||||
will-change: transform;
|
||||
@@ -1096,23 +1211,16 @@ body > #demo-frame {
|
||||
}
|
||||
|
||||
.saved-items-pane__close-btn {
|
||||
position: absolute;
|
||||
left: -18px;
|
||||
top: 24px;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
border-radius: 50%;
|
||||
padding: 10px 14px;
|
||||
background: crimson;
|
||||
color: white;
|
||||
border: 0;
|
||||
transform: scale(0);
|
||||
will-change: transform, opacity;
|
||||
transition: 0.3s ease;
|
||||
transition-property: transform, opacity;
|
||||
transition-delay: 0;
|
||||
}
|
||||
|
||||
.saved-items-pane.is-open .saved-items-pane__close-btn {
|
||||
opacity: 1;
|
||||
transition-delay: 0.4s;
|
||||
@@ -1124,8 +1232,6 @@ body > #demo-frame {
|
||||
padding: 20px;
|
||||
background-color: rgba(255, 255, 255, 0.06);
|
||||
position: relative;
|
||||
/*border: 1px solid rgba(255,255,255,0.1);*/
|
||||
margin: 20px 0;
|
||||
display: block;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
@@ -1135,6 +1241,10 @@ body > #demo-frame {
|
||||
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2);
|
||||
/* animation: slide-left 0.35s ease forwards; */
|
||||
}
|
||||
.saved-item-tile + .saved-item-tile {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.saved-item-tile--inline {
|
||||
display: inline-block;
|
||||
margin-left: 10px;
|
||||
@@ -1212,6 +1322,8 @@ body > #demo-frame {
|
||||
.saved-item-tile__btns {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
z-index: 1;
|
||||
right: 8px;
|
||||
opacity: 0;
|
||||
@@ -1230,14 +1342,21 @@ body > #demo-frame {
|
||||
}
|
||||
|
||||
.saved-item-tile__btn {
|
||||
display: inline-flex;
|
||||
padding: 7px 10px;
|
||||
color: white;
|
||||
border: 0;
|
||||
border-radius: 20px;
|
||||
border-radius: 1in;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
margin-left: 2px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.saved-item-tile__btn > svg {
|
||||
width: 1rem;
|
||||
aspect-ratio: 1;
|
||||
}
|
||||
|
||||
.saved-item-tile__btn:hover {
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
@@ -1260,7 +1379,9 @@ body > #demo-frame {
|
||||
.saved-items-pane__container {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
max-height: calc(100vh - 90px);
|
||||
max-height: calc(100vh - 130px);
|
||||
margin-top: 1rem;
|
||||
padding-inline: 1rem;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
@@ -1644,16 +1765,6 @@ body > #demo-frame {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.beta-tag {
|
||||
border-radius: 4px;
|
||||
text-transform: uppercase;
|
||||
background: #c68955;
|
||||
color: white;
|
||||
letter-spacing: 0.6px;
|
||||
padding: 2px 5px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.is-extension .web-maker-with-tag:after {
|
||||
display: none;
|
||||
}
|
||||
@@ -2008,6 +2119,256 @@ while the theme CSS file is loading */
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.asset-manager__upload-box {
|
||||
padding: 1rem 2rem;
|
||||
border: 3px dashed rgb(255 255 255 / 10%);
|
||||
border-radius: 1rem;
|
||||
text-align: center;
|
||||
margin-bottom: 1rem;
|
||||
transition: 0.3s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@keyframes move {
|
||||
0% {
|
||||
background-position: 0 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 50px 50px;
|
||||
}
|
||||
}
|
||||
|
||||
.asset-manager__progress-bar {
|
||||
position: absolute;
|
||||
inset: 1rem;
|
||||
/* height: 2rem; */
|
||||
background-image: linear-gradient(
|
||||
-45deg,
|
||||
rgba(255, 255, 255, 0.2) 25%,
|
||||
transparent 25%,
|
||||
transparent 50%,
|
||||
rgba(255, 255, 255, 0.2) 50%,
|
||||
rgba(255, 255, 255, 0.2) 75%,
|
||||
transparent 75%,
|
||||
transparent
|
||||
);
|
||||
z-index: 1;
|
||||
background-size: 50px 50px;
|
||||
animation: move 2s linear infinite;
|
||||
border-top-right-radius: 8px;
|
||||
border-bottom-right-radius: 8px;
|
||||
border-top-left-radius: 20px;
|
||||
border-bottom-left-radius: 20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.asset-manager__file {
|
||||
/* list-style: none; */
|
||||
/* padding: 0; */
|
||||
/* margin: 0; */
|
||||
display: flex;
|
||||
background: none;
|
||||
gap: 0.5rem;
|
||||
padding: 0.2rem;
|
||||
border-radius: 0.4rem;
|
||||
color: inherit;
|
||||
border: 0;
|
||||
transition: 0.3s ease;
|
||||
/* align-items: center; */
|
||||
}
|
||||
.asset-manager__file:hover {
|
||||
background: rgb(255 255 255 / 5%);
|
||||
}
|
||||
.asset-manager__file--grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/* align-items: flex-start; */
|
||||
}
|
||||
.asset-manager__file-container {
|
||||
padding: 0;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
.asset-manager__file-container--grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, minmax(90px, 1fr));
|
||||
max-width: 50rem;
|
||||
}
|
||||
|
||||
.asset-manager__file {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
position: relative;
|
||||
}
|
||||
.asset-manager__file-name {
|
||||
max-width: 100%;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
.asset-manager__file-image {
|
||||
height: 1.5rem;
|
||||
aspect-ratio: 1;
|
||||
border-radius: 0.3rem;
|
||||
object-fit: contain;
|
||||
}
|
||||
.asset-manager__file--grid .asset-manager__file-image {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
.asset-manager__file-ext {
|
||||
text-transform: uppercase;
|
||||
position: absolute;
|
||||
bottom: 0.5rem;
|
||||
left: 1.4rem;
|
||||
letter-spacing: -1px;
|
||||
font-weight: 800;
|
||||
display: none;
|
||||
}
|
||||
.asset-manager__file--grid .asset-manager__file-ext {
|
||||
display: block;
|
||||
}
|
||||
.asset-manager__file-actions {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
opacity: 0;
|
||||
padding: 0.5rem 0;
|
||||
visibility: hidden;
|
||||
transform: translateY(-0.5rem);
|
||||
transition: 0.3s ease;
|
||||
background-color: rgb(255 255 255 / 15%);
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
.asset-manager__file--grid .asset-manager__file-actions {
|
||||
left: 0;
|
||||
bottom: 1.5em;
|
||||
}
|
||||
.asset-manager__file:hover .asset-manager__file-actions {
|
||||
transform: translateY(0rem);
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
.stack {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.stack > * {
|
||||
margin: 0;
|
||||
}
|
||||
.badge {
|
||||
text-transform: uppercase;
|
||||
display: inline-block;
|
||||
padding: 0.1rem 0.3rem;
|
||||
background: rgb(255 255 255 / 40%);
|
||||
border-radius: 1rem;
|
||||
font-size: 0.75rem;
|
||||
color: #222;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
}
|
||||
.beta-tag {
|
||||
background: #ee9d59;
|
||||
}
|
||||
.pro-badge {
|
||||
display: inline-block;
|
||||
background: linear-gradient(45deg, var(--color-pro-1), var(--color-pro-2));
|
||||
color: #222;
|
||||
box-shadow: inset 2px 2px 3px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
.templates-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
}
|
||||
.templates-container .saved-item-tile {
|
||||
margin: 0;
|
||||
}
|
||||
.pro-modal {
|
||||
background: url('/assets/pro-panda-flying.png') no-repeat;
|
||||
background-position: bottom right;
|
||||
}
|
||||
.plan-card {
|
||||
background: rgb(255 255 255 / 10%);
|
||||
border-radius: 0.4rem;
|
||||
padding: 1rem;
|
||||
--shadow-color: 253deg 47% 15%;
|
||||
--shadow-elevation-low: 0.3px 0.5px 0.7px hsl(var(--shadow-color) / 0.34),
|
||||
0.4px 0.8px 1px -1.2px hsl(var(--shadow-color) / 0.34),
|
||||
1px 2px 2.5px -2.5px hsl(var(--shadow-color) / 0.34);
|
||||
--shadow-elevation-medium: 0.3px 0.5px 0.7px hsl(var(--shadow-color) / 0.36),
|
||||
0.8px 1.6px 2px -0.8px hsl(var(--shadow-color) / 0.36),
|
||||
2.1px 4.1px 5.2px -1.7px hsl(var(--shadow-color) / 0.36),
|
||||
5px 10px 12.6px -2.5px hsl(var(--shadow-color) / 0.36);
|
||||
|
||||
box-shadow: var(--shadow-elevation-low);
|
||||
}
|
||||
.profile-modal__avatar-img.is-pro {
|
||||
background: linear-gradient(45deg, var(--color-pro-1), var(--color-pro-2));
|
||||
padding: 0.2rem;
|
||||
animation: avatar-rotate 2s forwards;
|
||||
}
|
||||
@keyframes avatar-rotate {
|
||||
0% {
|
||||
transform: rotate(10deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
}
|
||||
.profile-modal__name.is-pro {
|
||||
background: linear-gradient(45deg, var(--color-pro-1), var(--color-pro-2));
|
||||
color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.profile-modal__panda {
|
||||
animation: slide-up 0.3s forwards;
|
||||
}
|
||||
|
||||
@keyframes slide-up {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateX(-1.5rem);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
}
|
||||
/* .PANEL */
|
||||
.panel {
|
||||
--panel-bg: rgb(255 255 255 / 5%);
|
||||
position: relative;
|
||||
background: var(--panel-bg);
|
||||
box-shadow: var(--panel-shadow);
|
||||
border-radius: 1rem;
|
||||
/* backdrop-filter: blur(20px); */
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.panelOnlyBorder {
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
backdrop-filter: none;
|
||||
border: 2px solid var(--clr-border-1);
|
||||
}
|
||||
|
||||
.panelGlowing {
|
||||
box-shadow: var(--glow-shadow);
|
||||
}
|
||||
|
||||
.panelTopFocus {
|
||||
background: radial-gradient(
|
||||
82.25% 100% at 50% 0%,
|
||||
rgba(var(--rgb-gray-1), 0.75) 37.28%,
|
||||
rgba(var(--rgb-gray-0), 0) 100%
|
||||
);
|
||||
|
||||
box-shadow:
|
||||
0 0 30px rgba(var(--rgb-brand), 0),
|
||||
0px 20px 50px rgba(0, 0, 0, 0.1),
|
||||
inset 0px 1px 3px rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
body {
|
||||
font-size: 70%;
|
||||
|
106
src/utils.js
106
src/utils.js
@@ -1,9 +1,9 @@
|
||||
import { trackEvent } from './analytics';
|
||||
|
||||
import { computeHtml, computeCss, computeJs } from './computes';
|
||||
import { modes, HtmlModes, CssModes, JsModes } from './codeModes';
|
||||
import { deferred } from './deferred';
|
||||
import { getExtensionFromFileName } from './fileUtils';
|
||||
import confetti from 'canvas-confetti';
|
||||
const esprima = require('esprima');
|
||||
|
||||
window.DEBUG = document.cookie.indexOf('wmdebug') > -1;
|
||||
@@ -23,7 +23,7 @@ export const BASE_PATH =
|
||||
window.DEBUG ||
|
||||
process.env.NODE_ENV === 'development'
|
||||
? '/'
|
||||
: '/app';
|
||||
: '/create';
|
||||
/* eslint-enable no-process-env */
|
||||
|
||||
var alphaNum = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||
@@ -205,6 +205,41 @@ export function getHumanDate(timestamp) {
|
||||
return retVal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert any date-ish string/obj to human readable form -> Jul 02, 2021
|
||||
* @param {string?object} date date to be formatted
|
||||
* @returns string
|
||||
*/
|
||||
export function getHumanReadableDate(
|
||||
date,
|
||||
{ showTime = true, utc = false } = {}
|
||||
) {
|
||||
if (!date) return '';
|
||||
let d = typeof date.toDate === 'function' ? date.toDate() : new Date(date);
|
||||
if (utc) {
|
||||
d = new Date(
|
||||
Date.UTC(d.getFullYear(), d.getMonth(), d.getDate(), d.getHours())
|
||||
);
|
||||
}
|
||||
|
||||
let options = {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
};
|
||||
if (showTime) {
|
||||
options = {
|
||||
...options,
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: true
|
||||
};
|
||||
}
|
||||
const dateTimeString = d.toLocaleString(false, options);
|
||||
return dateTimeString;
|
||||
}
|
||||
|
||||
// create a one-time event
|
||||
export function once(node, type, callback) {
|
||||
// create event
|
||||
@@ -343,19 +378,21 @@ export function getCompleteHtml(html, css, js, item, isForExport) {
|
||||
var externalJs = '',
|
||||
externalCss = '';
|
||||
if (item.externalLibs) {
|
||||
externalJs = item.externalLibs.js
|
||||
.split('\n')
|
||||
.reduce(function (scripts, url) {
|
||||
return scripts + (url ? '\n<script src="' + url + '"></script>' : '');
|
||||
}, '');
|
||||
externalCss = item.externalLibs.css
|
||||
.split('\n')
|
||||
.reduce(function (links, url) {
|
||||
return (
|
||||
links +
|
||||
(url ? '\n<link rel="stylesheet" href="' + url + '"></link>' : '')
|
||||
);
|
||||
}, '');
|
||||
externalJs = item.externalLibs.js.split('\n').reduce(function (
|
||||
scripts,
|
||||
url
|
||||
) {
|
||||
return scripts + (url ? '\n<script src="' + url + '"></script>' : '');
|
||||
}, '');
|
||||
externalCss = item.externalLibs.css.split('\n').reduce(function (
|
||||
links,
|
||||
url
|
||||
) {
|
||||
return (
|
||||
links +
|
||||
(url ? '\n<link rel="stylesheet" href="' + url + '"></link>' : '')
|
||||
);
|
||||
}, '');
|
||||
}
|
||||
var contents =
|
||||
'<!DOCTYPE html>\n' +
|
||||
@@ -585,3 +622,42 @@ if (window.IS_EXTENSION) {
|
||||
} else {
|
||||
document.body.classList.add('is-app');
|
||||
}
|
||||
export async function copyToClipboard(text) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
} catch (err) {
|
||||
console.error('Failed to copy text: ', err);
|
||||
}
|
||||
}
|
||||
|
||||
export function showConfetti(time = 4) {
|
||||
var end = Date.now() + time * 1000;
|
||||
|
||||
(function frame() {
|
||||
confetti({
|
||||
particleCount: 1,
|
||||
startVelocity: 0,
|
||||
ticks: 100,
|
||||
origin: {
|
||||
x: Math.random(),
|
||||
// since they fall down, start a bit higher than random
|
||||
y: Math.random() - 0.2
|
||||
},
|
||||
colors: [
|
||||
[
|
||||
'#26ccff',
|
||||
'#a25afd',
|
||||
'#ff5e7e',
|
||||
'#88ff5a',
|
||||
'#fcff42',
|
||||
'#ffa62d',
|
||||
'#ff36ff'
|
||||
][~~(Math.random() * 7)]
|
||||
]
|
||||
});
|
||||
|
||||
if (Date.now() < end) {
|
||||
requestAnimationFrame(frame);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
Reference in New Issue
Block a user