Skip to content

Commit

Permalink
add URL shortener
Browse files Browse the repository at this point in the history
  • Loading branch information
rjkat committed Apr 4, 2021
1 parent 314780e commit 57e5c7a
Show file tree
Hide file tree
Showing 7 changed files with 18,066 additions and 33 deletions.
2 changes: 1 addition & 1 deletion @confuzzle/confuz-parser/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ function parseClue(cw, clue) {
const textField = x.optionalField('text');
var parsedText = '';
if (textField) {
parsedText = textField.requiredStringValue();
parsedText = textField.optionalStringValue() || '';
}
const parsed = {
id: clueid,
Expand Down
71 changes: 65 additions & 6 deletions client/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@
:metadata="crossword.meta"
:shareLoading="shareLoading"
:shareLink="shareLink"
:shortLink="shortLink"
:emojiText="emojiNotation"
:sourceLoading="!state.initialised"
class="hidden-print"
:state="state"
:recentCrosswords="recentCrosswords"
Expand All @@ -78,6 +80,7 @@
@puz-file-uploaded="puzFileUploaded($event)"
@emoji-file-uploaded="emojiFileUploaded($event)"
@eno-file-uploaded="enoFileUploaded($event)"
@shorten-link-clicked="shortenLinkClicked($event)"
>
</cfz-header-toolbar>

Expand All @@ -90,8 +93,8 @@
<div id="drop-area" ref="dropArea">
<h1>Drop here to solve</h1>
</div>
<template v-if="state.joining">
<ui-modal ref="joinModal" @reveal="onJoinReveal()" :title="!joinFailed ? 'Join and solve' : 'Session not found'" :dismissible="state.joiningFromLauncher" @close="launcherJoinClosed()">
<template v-if="state.joining || state.downloading">
<ui-modal v-if="state.joining" ref="joinModal" @reveal="onJoinReveal()" :title="!joinFailed ? 'Join and solve' : 'Session not found'" :dismissible="state.joiningFromLauncher" @close="launcherJoinClosed()">
<div v-if="!joinFailed && !shouldJoin()" style="text-align: center;">
<p class="join-info-text">Join a crossword using a session identifier.</p>
<ui-textbox ref="sessionIdBox" class="crossword-join-input crossword-sess-id-input" v-model="sessionIdText" @keydown-enter="joinClicked()" autocomplete="off">
Expand All @@ -114,13 +117,22 @@
<ui-button color="primary" @click="dismissJoinError()">Dismiss</ui-button>
</div>
</ui-modal>
<ui-modal ref="downloadModal" v-else>
<div style="text-align: center;">
<p class="join-info-text">
Downloading crossword...
</p>
</div>
</ui-modal>
</template>
<template v-else>
<template v-if="state.initialised">
<transition name="grid">
<cfz-crossword-grid id="grid"
ref="grid"
key="gridContainer"
v-model="crossword"
:answerSlots.sync="answerSlots"
:workingLetters.sync="workingLetters"
:usingPencil="usingPencil"
:data-portrait="isPortrait"
:solverid="solverid"
Expand Down Expand Up @@ -188,6 +200,9 @@
</div>
</transition>
</template>
<template v-else>
<ui-progress-circular class="grid-loader"></ui-progress-circular>
</template>
</div>
<ui-fab v-if="exploding"
icon="close"
Expand Down Expand Up @@ -220,6 +235,10 @@ body {
*/
}
.grid-loader {
margin: auto;
}
.grid-enter-active,
.grid-leave-active {
transition: all 0.5s;
Expand Down Expand Up @@ -714,7 +733,10 @@ export default Vue.extend({
this.startJoining();
} else {
const params = new URLSearchParams(window.location.search);
this.firstLaunch = !(localStorage.haveLaunched || localStorage.recentCrosswords || params.get('source'));
if (params.get('puz') && params.get('puz').startsWith('http')) {
this.state.downloading = true;
}
this.firstLaunch = !(localStorage.haveLaunched || localStorage.recentCrosswords || params.get('source') || params.get('puz'));
this.state.launching = this.firstLaunch;
this.initSource();
}
Expand All @@ -735,6 +757,7 @@ export default Vue.extend({
recentCrosswords: [],
solverid: 0,
socketid: '',
shortLink: '',
showGrid: true,
usingPencil: false,
showTooltips: true,
Expand All @@ -749,7 +772,15 @@ export default Vue.extend({
emojiNotation: '',
lastId: '',
sessionIdText: "",
puzzleModalTitle: ''
puzzleModalTitle: '',
answerSlots: {
type: Object,
default: function () { return {} }
},
workingLetters: {
type: Object,
default: function () { return {} }
}
};
},
methods: {
Expand Down Expand Up @@ -1043,6 +1074,29 @@ export default Vue.extend({
updateTitle() {
document.title = this.pageTitle;
},
shortenLinkClicked(url) {
const xhr = new XMLHttpRequest();
xhr.open('POST', '/shorten');
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.addEventListener('load', event => {
this.shortLink = 'https://grids.confuzzle.me/' + event.target.responseText;
});
xhr.addEventListener('error', event => {
console.log(event.target.responseText);
});
xhr.send('uri=' + encodeURIComponent(url));
},
fetchPuz(url) {
this.state.downloading = true;
fetch(url).then(res => {
res.arrayBuffer().then(puz => {
this.state.downloading = false;
this.setCrosswordSource(confuz.fromPuz(ShareablePuz.from(Buffer.from(puz))))
})
}).catch(error => {
console.error('Error downloading puz:', error);
});
},
initSource() {
const params = new URLSearchParams(window.location.search);
const enoSource = params.get('source');
Expand All @@ -1060,7 +1114,11 @@ export default Vue.extend({
}
this.setCrosswordSource(eno);
} else if (puz) {
this.setCrosswordSource(confuz.fromPuz(ShareablePuz.fromURL(puz)));
if (puz.startsWith('https://')) {
this.fetchPuz(puz);
} else {
this.setCrosswordSource(confuz.fromPuz(ShareablePuz.fromURL(puz)));
}
} else if (strippedPuz) {
this.setCrosswordSource(confuz.fromPuz(ShareablePuz.fromEmoji(strippedPuz, true)));
} else if (localStorage.crosswordId) {
Expand Down Expand Up @@ -1578,6 +1636,7 @@ export default Vue.extend({
setCrosswordSource(source) {
this.crosswordSource = source;
this.editorSource = source;
this.state.initialised = true;
},
importEmojiClicked(emoji) {
this.setCrosswordSource(confuz.fromPuz(ShareablePuz.fromEmoji(emoji, true)));
Expand Down
74 changes: 64 additions & 10 deletions client/components/CfzHeaderToolbar.vue
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
<template>
<ui-toolbar type="colored" class="crossword-toolbar" style="overflow: hidden;" removeNavIcon>
<ui-toolbar type="colored" class="crossword-toolbar" :loading="sourceLoading" style="overflow: hidden;" removeNavIcon>
<table slot="brand" width="40">
<td data-solver-mask="3" data-number="?" style="height: 1em; width: 1em; z-index: 0 !important;"
@click="$emit('logo-clicked')">C</td>
</table>
<template v-slot="title">
<div class="crossword-title">
<div v-if="!sourceLoading" class="crossword-title">
<span class="crossword-meta-name" v-responsive.class>{{metadata.name}}</span>
<span class="crossword-meta-author" v-responsive.class>by {{metadata.author}}</span>
<span class="crossword-meta-identifier" v-if="metadata.identifier" v-responsive.md.lg.xl>{{metadata.identifier}}</span>
</div>
<div v-else class="crossword-loading-text">Loading...</div>
</template>
<div slot="actions" class="hidden-print crossword-toolbar-actions">
<div v-if="!sourceLoading" slot="actions" class="hidden-print crossword-toolbar-actions">
<ui-icon-button
color="white"
icon="print"
Expand Down Expand Up @@ -77,6 +78,26 @@
</li>
</ul>
</ui-modal>
<ui-modal ref="linkModal" title="Link external crossword">
<div style="text-align: center;">
<template v-if="!shortLink">
<div v-if="!shortLink">
<p class="about-text">
Generate a convenient link to a crossword hosted elsewhere.
</p>
<ui-textbox class="crossword-join-input crossword-sess-id-input" v-model="externalLink" @keydown-enter="shortenLinkClicked()" autocomplete="off" :invalid="linkInvalid" error="Invalid URL">
URL of .puz or .confuz
</ui-textbox>
<ui-button color="primary" style="margin-top: 1em;" :loading="creatingLink" @click="shortenLinkClicked()" :disabled="linkInvalid">Submit</ui-button>
</div>
</template>
<template v-else>
<p class="about-text">Access your externally-hosted crossword using the following link.</p>
<div class="crossword-link-text">{{shortLink}}</div>
<ui-button color="primary" style="margin-top: 1em;" @click="copyClicked()">Copy</ui-button>
</template>
</div>
</ui-modal>
<ui-modal ref="aboutModal" title="About">
<div style="text-align: center;">
<p class="about-text">
Expand Down Expand Up @@ -167,6 +188,12 @@ ul {
margin-bottom: .25em;
}
.crossword-loading-text {
text-transform: uppercase;
font-family: $titleFontFamily;
font-weight: bold;
}
.crossword-meta-name {
text-transform: uppercase;
font-family: $titleFontFamily;
Expand Down Expand Up @@ -200,6 +227,8 @@ import {emojisplosion} from "emojisplosion";
import CfzShareModal from './CfzShareModal.vue'
import CfzFileInput from './CfzFileInput.vue'
import copy from 'copy-to-clipboard';
// https://gist.github.com/hanayashiki/8dac237671343e7f0b15de617b0051bd
(function () {
if ('File' in self)
Expand Down Expand Up @@ -246,6 +275,14 @@ function explodeOn(id) {
})
}
// https://stackoverflow.com/a/22648406
function isURL(str) {
var urlRegex = '^(?!mailto:)(?:(?:http|https|ftp)://)(?:\\S+(?::\\S*)?@)?(?:(?:(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}(?:\\.(?:[0-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))|(?:(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)(?:\\.(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)*(?:\\.(?:[a-z\\u00a1-\\uffff]{2,})))|localhost)(?::\\d{2,5})?(?:(/|\\?|#)[^\\s]*)?$';
var url = new RegExp(urlRegex, 'i');
return str.length < 2083 && url.test(str);
}
export default Vue.extend({
components: {
CfzShareModal,
Expand All @@ -257,10 +294,15 @@ export default Vue.extend({
recentCrosswords: Array,
shareLoading: false,
shareLink: "",
shortLink: "",
sourceLoading: false,
showInstall: false,
emojiText: ""
},
computed: {
linkInvalid() {
return !isURL(this.externalLink);
},
recentMetas() {
const metas = [];
for (const cwid of this.recentCrosswords) {
Expand All @@ -287,7 +329,7 @@ export default Vue.extend({
options.push(this.opt.SAVE_PUZ);
options.push(this.opt.SAVE_ENO);
// options.push(this.opt.EXPORT_ENO_LINK);
options.push(this.opt.LINK_EXTERNAL);
options.push(this.opt.ABOUT);
return options;
Expand All @@ -314,6 +356,10 @@ export default Vue.extend({
this.openModal('emojiModal');
this.closeModal('aboutModal');
},
copyClicked() {
copy(this.shortLink);
this.$emit('copy-clicked');
},
copyEmojiClicked() {
explodeOn('copy-emoji-button');
this.$emit('copy-emoji-clicked');
Expand All @@ -334,19 +380,25 @@ export default Vue.extend({
this.$emit('open-recent-clicked', id);
this.closeModal('recentModal');
},
shortenLinkClicked() {
if (this.linkInvalid)
return;
this.creatingLink = true;
this.$emit('shorten-link-clicked', this.externalLink);
},
selectMenuOption(option) {
if (option.label == this.opt.SAVE_PUZ.label) {
this.$emit('download-puz-clicked');
} else if (option.label == this.opt.SAVE_ENO.label) {
this.$emit('download-eno-clicked');
} else if (option.label == this.opt.EXPORT_ENO_LINK.label) {
this.$emit('export-eno-clicked');
} else if (option.label == this.opt.SOLVE_OFFLINE.label) {
this.$emit('go-offline-clicked');
} else if (option.label == this.opt.OPEN_PUZZLE.label) {
this.openPuzzle();
} else if (option.label == this.opt.OPEN_RECENT.label) {
} else if (option.label == this.opt.OPEN_RECENT.label) {
this.openModal('recentModal');
} else if (option.label == this.opt.LINK_EXTERNAL.label) {
this.openModal('linkModal');
} else if (option.label == this.opt.ABOUT.label) {
this.openModal('aboutModal');
} else if (option.label == this.opt.INSTALL.label) {
Expand All @@ -367,7 +419,9 @@ export default Vue.extend({
return {
bundler: "Parcel",
isOnline: true,
creatingLink: false,
inputEmoji: "",
externalLink: "",
opt: {
INSTALL: {
label: 'Install app',
Expand All @@ -389,9 +443,9 @@ export default Vue.extend({
label: 'Save as .confuz',
icon: 'get_app'
},
EXPORT_ENO_LINK: {
label: 'Save as link',
icon: 'get_app'
LINK_EXTERNAL: {
label: 'Link external...',
icon: 'link'
},
SOLVE_OFFLINE: {
label: 'Leave session',
Expand Down
Loading

0 comments on commit 57e5c7a

Please sign in to comment.