diff --git a/.travis.yml b/.travis.yml index 8aa80c5b5..8a7d2ff69 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,7 +16,7 @@ node_js: - "12" env: - - DEPLOY_ENV=GH_PAGES + - DEPLOY_ENV=POSTWOMAN_IO cache: directories: diff --git a/README.md b/README.md index 6f6b399c9..2b19a51f3 100644 --- a/README.md +++ b/README.md @@ -23,8 +23,8 @@ When I wrote this, only God and I understood what I was doing. Now, only God kno

- postwoman - postwoman + postwoman + postwoman
@@ -63,6 +63,15 @@ _Customized themes are also synced with local session storage_ - Offline support - Low RAM/memory and CPU usage +:rocket: **Request**: Retrieve data from a URL without having to do a full page refresh + + - Choose `method` + - Enter `URL` + - Enter `Path` + - Copy public "Share URL" + - Generate request code for JavaScript XHR, Fetch, cURL + - Copy generated request code to clipboard + :electric_plug: **Web Socket**: Establish full-duplex communication channels over a single TCP connection - Send and receive data @@ -86,6 +95,9 @@ _Customized themes are also synced with local session storage_ :wave: **Responses**: Contains the status line, headers and the message/response body + - Copy response to clipboard + - View preview for HTML responses + _HTML responses have "Preview HTML" feature_ :alarm_clock: **History**: Request entries are synced with local session storage to reuse with a single click @@ -103,7 +115,9 @@ _History entries can be deleted one-by-one or all together_ ## Demo -[https://liyasthomas.github.io/postwoman](https://liyasthomas.github.io/postwoman) +[https://postwoman.io](https://postwoman.io) + +## Usage 1. Specify your request method 2. Type in your API URL @@ -177,20 +191,27 @@ See the [CHANGELOG](CHANGELOG.md) file for details. * ([contributors](https://github.com/liyasthomas/postwoman/graphs/contributors)) ### Contributors -* [John Harker](https://github.com/NBTX) -* [Andrew Bastin](https://github.com/AndrewBastin) -* [Nick Palenchar](https://github.com/nickpalenchar) -* [Abraham Williams](https://github.com/abraham) -* [Nicholas La Roux](https://github.com/larouxn) -* [RifqiAlAbqary](https://github.com/reefqi037) -* [izerozlu](https://github.com/izerozlu) -* [Thomas Yuba](https://github.com/yubathom) + + + + + + + + + + + + +
John Harker
John Harker

💻
izerozlu
izerozlu

💻
Andrew Bastin
Andrew Bastin

💻
Nick Palenchar
Nick Palenchar

💻
Thomas Yuba
Thomas Yuba

💻
Nicholas La Roux
Nicholas La Roux

💻
+ + + +See the list of [contributors](https://github.com/liyasthomas/postwoman/graphs/contributors) who participated in this project. ### Thanks * [Dribbble](https://dribbble.com) -See the list of [contributors](https://github.com/liyasthomas/postwoman/graphs/contributors) who participated in this project. - --- ## License diff --git a/assets/css/styles.scss b/assets/css/styles.scss index 541bbbe4c..b276105cb 100644 --- a/assets/css/styles.scss +++ b/assets/css/styles.scss @@ -10,7 +10,11 @@ $responsiveWidth: 720px; } ::-webkit-scrollbar-thumb { - background-color: #4a4a4a; + background-color: rgba(0, 0, 0, .3); +} + +::-webkit-scrollbar-thumb:hover { + background-color: rgba(0, 0, 0, .5); } * { @@ -78,6 +82,9 @@ body.sticky-footer footer { } button { + display: inline-flex; + align-items: center; + justify-content: center; margin: 4px; padding: 8px 16px; border-radius: 4px; @@ -88,9 +95,19 @@ button { cursor: pointer; transition: all 0.2s ease-in-out; + &.icon { + background-color: var(--bg-color); + color: var(--ac-color); + fill: var(--ac-color); + + span { + margin-left: 8px; + } + } + &:not([disabled]):hover, &:not(.disabled):focus { - background-color: transparent; + background-color: var(--bg-color); box-shadow: inset 0 0 0 2px var(--ac-color); color: var(--ac-color); } @@ -122,51 +139,59 @@ fieldset.blue legend { } fieldset.gray { - border-color: #9B9B9B; + border-color: #BCC2CD; } fieldset.gray legend { - color: #9B9B9B; + color: #BCC2CD; } fieldset.green { - border-color: #B8E986; + border-color: #50fa7b; } fieldset.green legend { - color: #B8E986; + color: #50fa7b; } fieldset.cyan { - border-color: #50E3C2; + border-color: #8be9fd; } fieldset.cyan legend { - color: #50E3C2; -} - -fieldset.blue-dark { - border-color: #4A90E2; -} - -fieldset.blue-dark legend { - color: #4A90E2; + color: #8be9fd; } fieldset.purple { - border-color: #C198FB; + border-color: #bd93f9; } fieldset.purple legend { - color: #C198FB; + color: #bd93f9; } fieldset.orange { - border-color: #F5A623; + border-color: #ffb86c; } fieldset.orange legend { - color: #F5A623; + color: #ffb86c; +} + +fieldset.pink { + border-color: #ff79c6; +} + +fieldset.pink legend { + color: #ff79c6; +} + +fieldset.red { + border-color: #ff5555; +} + +fieldset.red legend { + color: #ff5555; } .hidden { @@ -187,12 +212,19 @@ pre { font-weight: 700; font-size: 18px; font-family: monospace; + transition: all 0.2s ease-in-out; } select, input, option { height: 41px; + + &:not([readonly]):hover, + &:not([readonly]):focus { + background-color: var(--bg-color); + box-shadow: inset 0 0 0 2px var(--ac-color); + } } input[type="checkbox"] { @@ -231,6 +263,11 @@ input[type="checkbox"] { background-color: var(--err-color); color: #b2b2b2; cursor: default; + + &.icon { + color: #b2b2b2; + fill: #b2b2b2; + } } label { @@ -258,6 +295,10 @@ ol li { align-items: center; } +.show-on-small-screen { + display: flex; +} + @media (max-width: $responsiveWidth) { header div { display: flex; @@ -283,6 +324,10 @@ ol li { .hide-on-small-screen { display: none; } + + .show-on-small-screen { + display: inline-flex; + } } #installPWA { @@ -349,11 +394,8 @@ fieldset#history { margin: 4px; textarea { - width: 100%; - } - - #response-details { margin: 0; + width: 100%; } .covers-response { diff --git a/assets/css/themes.scss b/assets/css/themes.scss index d611740f3..4428f363c 100644 --- a/assets/css/themes.scss +++ b/assets/css/themes.scss @@ -7,37 +7,37 @@ // Dark is the default theme variant. :root { - --bg-dark-color: #000000; + --bg-dark-color: #44475a; // Background color - --bg-color: #121212; - // Auto-complete color - --atc-color: #212121; + --bg-color: #282a36; + // Auto-complete color + --atc-color: #3C4556; // Text color - --fg-color: #FFF; + --fg-color: #f8f8f2; // Error color - --err-color: #393939; + --err-color: #3C4556; // Active color - --ac-color: #51FF0D; + --ac-color: #50fa7b; // Active text color - --act-color: #121212; + --act-color: #282a36; } :root.light { - --bg-dark-color: #ffffff; + --bg-dark-color: #e1e4eb; // Background color - --bg-color: #F6F8FA; - // Auto-complete color - --atc-color: #F1F1F1; + --bg-color: #ebeef5; + // Auto-complete color + --atc-color: #e1e4eb; // Text color - --fg-color: #121212; + --fg-color: #5d5d5f; // Error color - --err-color: invert(#393939, 1); + --err-color: invert(#3C4556, 1); // Active color - --ac-color: #51FF0D; + --ac-color: #57b5f9; // Active text color - --act-color: #121212; + --act-color: #ebeef5; } diff --git a/components/autocomplete.vue b/components/autocomplete.vue index 0ec662627..f985d8718 100644 --- a/components/autocomplete.vue +++ b/components/autocomplete.vue @@ -1,190 +1,186 @@ diff --git a/components/history.vue b/components/history.vue index dca3a8993..592b5571e 100644 --- a/components/history.vue +++ b/components/history.vue @@ -3,7 +3,7 @@ @@ -25,18 +25,24 @@ -
  • - - -
  • -
  • - - -
  • +
    +
  • + + +
  • +
  • + + +
  • +
    - +
    1. @@ -278,6 +339,8 @@ data() { return { showModal: false, + copyButton: '', + copiedButton: '', method: 'GET', url: 'https://reqres.in', auth: 'None', @@ -291,6 +354,8 @@ rawParams: '', rawInput: false, contentType: 'application/json', + requestType: 'JavaScript XHR', + isHidden: true, response: { status: '', headers: '', @@ -323,6 +388,10 @@ watch: { contentType(val) { this.rawInput = !this.knownContentTypes.includes(val); + }, + rawInput (status) { + if (status && this.rawParams === '') this.rawParams = '{}' + else this.setRouteQueryState() } }, computed: { @@ -386,6 +455,89 @@ }, responseType() { return (this.response.headers['content-type'] || '').split(';')[0].toLowerCase(); + }, + requestCode() { + if (this.requestType == 'JavaScript XHR') { + var requestString = [] + requestString.push('const xhr = new XMLHttpRequest()'); + const user = this.auth === 'Basic' ? this.httpUser : null + const pswd = this.auth === 'Basic' ? this.httpPassword : null + requestString.push('xhr.open(' + this.method + ', ' + this.url + this.path + this.queryString + ', true, ' + user + ', ' + pswd + ')'); + if (this.auth === 'Bearer Token') { + requestString.push("xhr.setRequestHeader('Authorization', 'Bearer ' + " + this.bearerToken + ")"); + } + if (this.headers) { + this.headers.forEach(function(element) { + requestString.push('xhr.setRequestHeader(' + element.key + ', ' + element.value + ')'); + }) + } + if (this.method === 'POST' || this.method === 'PUT') { + const requestBody = this.rawInput ? this.rawParams : this.rawRequestBody; + requestString.push("xhr.setRequestHeader('Content-Length', " + requestBody.length + ")") + requestString.push("xhr.setRequestHeader('Content-Type', `" + this.contentType + "; charset=utf-8`)") + requestString.push("xhr.send(" + requestBody + ")") + } else { + requestString.push('xhr.send()') + } + return requestString.join('\n'); + } else if (this.requestType == 'Fetch') { + var requestString = []; + var headers = []; + requestString.push('fetch(' + this.url + this.path + this.queryString + ', {\n') + requestString.push(' method: "' + this.method + '",\n') + if (this.auth === 'Basic') { + var basic = this.httpUser + ':' + this.httpPassword; + headers.push(' "Authorization": "Basic ' + window.btoa(unescape(encodeURIComponent(basic))) + ',\n') + } else if (this.auth === 'Bearer Token') { + headers.push(' "Authorization": "Bearer Token ' + this.bearerToken + ',\n') + } + if (this.method === 'POST' || this.method === 'PUT') { + const requestBody = this.rawInput ? this.rawParams : this.rawRequestBody; + requestString.push(' body: ' + requestBody + ',\n') + headers.push(' "Content-Length": ' + requestBody.length + ',\n') + headers.push(' "Content-Type": "' + this.contentType + '; charset=utf-8",\n') + } + if (this.headers) { + this.headers.forEach(function(element) { + headers.push(' "' + element.key + '": "' + element.value + '",\n'); + }) + } + headers = headers.join('').slice(0, -3); + requestString.push(' headers: {\n' + headers + '\n },\n') + requestString.push(' credentials: "same-origin"\n') + requestString.push(')}).then(function(response) {\n') + requestString.push(' response.status\n') + requestString.push(' response.statusText\n') + requestString.push(' response.headers\n') + requestString.push(' response.url\n\n') + requestString.push(' return response.text()\n') + requestString.push(')}, function(error) {\n') + requestString.push(' error.message\n') + requestString.push(')}') + return requestString.join(''); + } else if (this.requestType == 'cURL') { + var requestString = []; + requestString.push('curl -X ' + this.method + ' \\\n') + requestString.push(" '" + this.url + this.path + this.queryString + "' \\\n") + if (this.auth === 'Basic') { + var basic = this.httpUser + ':' + this.httpPassword; + requestString.push(" -H 'Authorization: Basic " + window.btoa(unescape(encodeURIComponent(basic))) + "' \\\n") + } else if (this.auth === 'Bearer Token') { + requestString.push(" -H 'Authorization: Bearer Token " + this.bearerToken + "' \\\n") + } + if (this.headers) { + this.headers.forEach(function(element) { + requestString.push(" -H '" + element.key + ": " + element.value + "' \\\n"); + }) + } + if (this.method === 'POST' || this.method === 'PUT') { + const requestBody = this.rawInput ? this.rawParams : this.rawRequestBody; + requestString.push(" -H 'Content-Length: " + requestBody.length + "' \\\n") + requestString.push(" -H 'Content-Type: " + this.contentType + "; charset=utf-8' \\\n") + requestString.push(" -d '" + requestBody + "' \\\n") + } + return requestString.join('').slice(0, -4); + } } }, methods: { @@ -561,17 +713,38 @@ } }, copyRequest() { - var dummy = document.createElement('input'); - document.body.appendChild(dummy); - dummy.value = window.location.href; - dummy.select(); - document.execCommand('copy'); - document.body.removeChild(dummy); + if (navigator.share) { + let time = new Date().toLocaleTimeString(); + let date = new Date().toLocaleDateString(); + navigator.share({ + text: `Postwoman • API request builder at ${time} on ${date}`, + url: window.location.href + }).then(() => { + // console.log('Thanks for sharing!'); + }) + .catch(console.error); + } else { + this.$refs.copyRequest.innerHTML = this.copiedButton + 'Copied'; + var dummy = document.createElement('input'); + document.body.appendChild(dummy); + dummy.value = window.location.href; + dummy.select(); + document.execCommand('copy'); + document.body.removeChild(dummy); + setTimeout(() => this.$refs.copyRequest.innerHTML = this.copyButton + 'Share URL', 1500) + } + }, + copyRequestCode() { + this.$refs.copyRequestCode.innerHTML = this.copiedButton + 'Copied'; + this.$refs.generatedCode.select(); + document.execCommand("copy"); + setTimeout(() => this.$refs.copyRequestCode.innerHTML = this.copyButton + 'Copy', 1500) }, copyResponse() { - var copyText = document.getElementById("response-details"); - copyText.select(); + this.$refs.copyResponse.innerHTML = this.copiedButton + 'Copied'; + this.$refs.responseBody.select(); document.execCommand("copy"); + setTimeout(() => this.$refs.copyResponse.innerHTML = this.copyButton + 'Copy', 1500) }, togglePreview() { this.previewEnabled = !this.previewEnabled; @@ -601,14 +774,19 @@ } else return '' } let flats = ['method', 'url', 'path', 'auth', 'httpUser', 'httpPassword', 'bearerToken', 'contentType'].map(item => flat(item)) - let deeps = ['headers', 'params', 'bodyParams'].map(item => deep(item)) - this.$router.replace('/?' + flats.concat(deeps).join('').slice(0, -1)) + let deeps = ['headers', 'params'].map(item => deep(item)) + let bodyParams = this.rawInput ? [flat('rawParams')] : [deep('bodyParams')]; + + this.$router.replace('/?' + flats.concat(deeps, bodyParams).join('').slice(0, -1)) }, setRouteQueries(queries) { if (typeof(queries) !== 'object') throw new Error('Route query parameters must be a Object') for (const key in queries) { if (key === 'headers' || key === 'params' || key === 'bodyParams') this[key] = JSON.parse(queries[key]) - else if (typeof(this[key]) === 'string') this[key] = queries[key]; + if (key === 'rawParams') { + this.rawInput = true + this.rawParams = queries['rawParams'] + } else if (typeof(this[key]) === 'string') this[key] = queries[key] } }, observeRequestButton() { @@ -665,11 +843,11 @@ vm.headers, vm.params, vm.bodyParams, - vm.contentType + vm.contentType, + vm.rawParams ], val => { this.setRouteQueryState() }) - } } diff --git a/pages/settings.vue b/pages/settings.vue index e16dce280..7786e0a98 100644 --- a/pages/settings.vue +++ b/pages/settings.vue @@ -1,37 +1,39 @@ diff --git a/pages/websocket.vue b/pages/websocket.vue index eb731ac24..641e90942 100644 --- a/pages/websocket.vue +++ b/pages/websocket.vue @@ -1,6 +1,6 @@