1
0
mirror of https://github.com/chinchang/web-maker.git synced 2025-07-27 08:40:10 +02:00

Merge pull request #553 from chinchang/v6

V6
This commit is contained in:
Kushagra Gour
2024-04-28 23:16:19 +05:30
committed by GitHub
48 changed files with 8751 additions and 15973 deletions

View File

@@ -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();
});

View File

@@ -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
[[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

File diff suppressed because it is too large Load Diff

View File

@@ -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": {

View File

@@ -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>

View File

@@ -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">

View File

@@ -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);
}
}
}

View 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
View 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>

View File

@@ -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;

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

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
View 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 };

View File

@@ -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();
});

View File

@@ -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} />;
}
}

View File

@@ -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)} />

View File

@@ -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) {

View File

@@ -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>

View File

@@ -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>
&copy;
<span class="web-maker-with-tag">Web Maker</span> &nbsp;&nbsp;
<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>
&copy;
<span class="web-maker-with-tag">Web Maker</span> &nbsp;&nbsp;
<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>
);
};

View File

@@ -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>
);

View File

@@ -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>
);
};

View File

@@ -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
View 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>
);
}

View File

@@ -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>
);

View File

@@ -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>

View File

@@ -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>
);
};

View File

@@ -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>
&nbsp;
<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>
&nbsp;
<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
View 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
View 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>
);
};

View File

@@ -0,0 +1,3 @@
export const ProBadge = () => {
return <div className="badge pro-badge">PRO</div>;
};

View File

@@ -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>
);
}

View File

@@ -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
View 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
View 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 };

View File

@@ -1,5 +1,3 @@
import { h } from 'preact';
export default function Switch({
checked,
onChange,

68
src/components/Text.jsx Normal file
View 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>
);
}
);

View File

@@ -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 || [])}

View File

@@ -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>;
}

View File

@@ -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
View 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 };

View File

@@ -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>

View File

@@ -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
View 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>

View File

@@ -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

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html>
<head>
<title>Settings - Web Maker</title>

View File

@@ -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%;

View File

@@ -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);
}
})();
}