summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--controllers/auth.php161
-rw-r--r--controllers/controllers.php159
-rw-r--r--lib/helpers.php33
-rw-r--r--public/js/script.js8
-rw-r--r--views/dashboard.php20
-rw-r--r--views/index.php19
-rw-r--r--views/new-bookmark.php10
-rw-r--r--views/new-post.php40
-rw-r--r--views/review.php257
9 files changed, 517 insertions, 190 deletions
diff --git a/controllers/auth.php b/controllers/auth.php
index a02e047..6af9ac4 100644
--- a/controllers/auth.php
+++ b/controllers/auth.php
@@ -4,19 +4,6 @@ function buildRedirectURI($params = array()) {
return Config::$base_url . 'auth/callback?' . http_build_query($params);
}
-function build_url($parsed_url) {
- $scheme = isset($parsed_url['scheme']) ? $parsed_url['scheme'] . '://' : '';
- $host = isset($parsed_url['host']) ? $parsed_url['host'] : '';
- $port = isset($parsed_url['port']) ? ':' . $parsed_url['port'] : '';
- $user = isset($parsed_url['user']) ? $parsed_url['user'] : '';
- $pass = isset($parsed_url['pass']) ? ':' . $parsed_url['pass'] : '';
- $pass = ($user || $pass) ? "$pass@" : '';
- $path = isset($parsed_url['path']) ? $parsed_url['path'] : '';
- $query = isset($parsed_url['query']) ? '?' . $parsed_url['query'] : '';
- $fragment = isset($parsed_url['fragment']) ? '#' . $parsed_url['fragment'] : '';
- return "$scheme$user$pass$host$port$path$query$fragment";
-}
-
$app->get('/', function($format='html') use($app) {
$res = $app->response();
$params = $app->request()->params();
@@ -264,3 +251,151 @@ $app->get('/signout', function() use($app) {
unset($_SESSION['user_id']);
$app->redirect('/', 301);
});
+
+
+/*
+$app->post('/auth/facebook', function() use($app) {
+ if($user=require_login($app, false)) {
+ $params = $app->request()->params();
+ // User just auth'd with facebook, store the access token
+ $user->facebook_access_token = $params['fb_token'];
+ $user->save();
+
+ $app->response()->body(json_encode(array(
+ 'result' => 'ok'
+ )));
+ } else {
+ $app->response()->body(json_encode(array(
+ 'result' => 'error'
+ )));
+ }
+});
+*/
+
+$app->post('/auth/twitter', function() use($app) {
+ if($user=require_login($app, false)) {
+ $params = $app->request()->params();
+ // User just auth'd with twitter, store the access token
+ $user->twitter_access_token = $params['twitter_token'];
+ $user->twitter_token_secret = $params['twitter_secret'];
+ $user->save();
+
+ $app->response()['Content-type'] = 'application/json';
+ $app->response()->body(json_encode(array(
+ 'result' => 'ok'
+ )));
+ } else {
+ $app->response()['Content-type'] = 'application/json';
+ $app->response()->body(json_encode(array(
+ 'result' => 'error'
+ )));
+ }
+});
+
+function getTwitterLoginURL(&$twitter) {
+ $request_token = $twitter->getRequestToken(Config::$base_url . 'auth/twitter/callback');
+ $_SESSION['twitter_auth'] = $request_token;
+ return $twitter->getAuthorizeURL($request_token['oauth_token']);
+}
+
+$app->get('/auth/twitter', function() use($app) {
+ $params = $app->request()->params();
+ if($user=require_login($app, false)) {
+
+ // If there is an existing Twitter token, check if it is valid
+ // Otherwise, generate a Twitter login link
+ $twitter_login_url = false;
+ $twitter = new \TwitterOAuth\Api(Config::$twitterClientID, Config::$twitterClientSecret,
+ $user->twitter_access_token, $user->twitter_token_secret);
+
+ if(array_key_exists('login', $params)) {
+ $twitter = new \TwitterOAuth\Api(Config::$twitterClientID, Config::$twitterClientSecret);
+ $twitter_login_url = getTwitterLoginURL($twitter);
+ } else {
+ if($user->twitter_access_token) {
+ if ($twitter->get('account/verify_credentials')) {
+ $app->response()['Content-type'] = 'application/json';
+ $app->response()->body(json_encode(array(
+ 'result' => 'ok'
+ )));
+ return;
+ } else {
+ // If the existing twitter token is not valid, generate a login link
+ $twitter_login_url = getTwitterLoginURL($twitter);
+ }
+ } else {
+ $twitter_login_url = getTwitterLoginURL($twitter);
+ }
+ }
+
+ $app->response()['Content-type'] = 'application/json';
+ $app->response()->body(json_encode(array(
+ 'url' => $twitter_login_url
+ )));
+
+ } else {
+ $app->response()['Content-type'] = 'application/json';
+ $app->response()->body(json_encode(array(
+ 'result' => 'error'
+ )));
+ }
+});
+
+$app->get('/auth/twitter/callback', function() use($app) {
+ if($user=require_login($app)) {
+ $params = $app->request()->params();
+
+ $twitter = new \TwitterOAuth\Api(Config::$twitterClientID, Config::$twitterClientSecret,
+ $_SESSION['twitter_auth']['oauth_token'], $_SESSION['twitter_auth']['oauth_token_secret']);
+ $credentials = $twitter->getAccessToken($params['oauth_verifier']);
+
+ $user->twitter_access_token = $credentials['oauth_token'];
+ $user->twitter_token_secret = $credentials['oauth_token_secret'];
+ $user->twitter_username = $credentials['screen_name'];
+ $user->save();
+
+ $app->redirect('/settings');
+ }
+});
+
+$app->get('/auth/instagram', function() use($app) {
+ if($user=require_login($app, false)) {
+
+ $instagram = instagram_client();
+
+ // If there is an existing Instagram auth token, check if it's valid
+ if($user->instagram_access_token) {
+ $instagram->setAccessToken($user->instagram_access_token);
+ $igUser = $instagram->getUser();
+
+ if($igUser && $igUser->meta->code == 200) {
+ $app->response()['Content-type'] = 'application/json';
+ $app->response()->body(json_encode(array(
+ 'result' => 'ok',
+ 'username' => $igUser->data->username,
+ 'url' => $instagram->getLoginUrl(array('basic','likes'))
+ )));
+ return;
+ }
+ }
+
+ $app->response()['Content-type'] = 'application/json';
+ $app->response()->body(json_encode(array(
+ 'result' => 'error',
+ 'url' => $instagram->getLoginUrl(array('basic','likes'))
+ )));
+ }
+});
+
+$app->get('/auth/instagram/callback', function() use($app) {
+ if($user=require_login($app)) {
+ $params = $app->request()->params();
+
+ $instagram = instagram_client();
+ $data = $instagram->getOAuthToken($params['code']);
+ $user->instagram_access_token = $data->access_token;
+ $user->save();
+
+ $app->redirect('/settings');
+ }
+});
diff --git a/controllers/controllers.php b/controllers/controllers.php
index 7565b4d..d6cad4f 100644
--- a/controllers/controllers.php
+++ b/controllers/controllers.php
@@ -178,6 +178,18 @@ $app->get('/photo', function() use($app) {
}
});
+$app->get('/review', function() use($app) {
+ if($user=require_login($app)) {
+ $params = $app->request()->params();
+
+ $html = render('review', array(
+ 'title' => 'Review',
+ 'authorizing' => false
+ ));
+ $app->response()->body($html);
+ }
+});
+
$app->get('/repost', function() use($app) {
if($user=require_login($app)) {
$params = $app->request()->params();
@@ -571,150 +583,3 @@ $app->post('/micropub/postjson', function() use($app) {
)));
}
});
-
-/*
-$app->post('/auth/facebook', function() use($app) {
- if($user=require_login($app, false)) {
- $params = $app->request()->params();
- // User just auth'd with facebook, store the access token
- $user->facebook_access_token = $params['fb_token'];
- $user->save();
-
- $app->response()->body(json_encode(array(
- 'result' => 'ok'
- )));
- } else {
- $app->response()->body(json_encode(array(
- 'result' => 'error'
- )));
- }
-});
-*/
-
-$app->post('/auth/twitter', function() use($app) {
- if($user=require_login($app, false)) {
- $params = $app->request()->params();
- // User just auth'd with twitter, store the access token
- $user->twitter_access_token = $params['twitter_token'];
- $user->twitter_token_secret = $params['twitter_secret'];
- $user->save();
-
- $app->response()['Content-type'] = 'application/json';
- $app->response()->body(json_encode(array(
- 'result' => 'ok'
- )));
- } else {
- $app->response()['Content-type'] = 'application/json';
- $app->response()->body(json_encode(array(
- 'result' => 'error'
- )));
- }
-});
-
-function getTwitterLoginURL(&$twitter) {
- $request_token = $twitter->getRequestToken(Config::$base_url . 'auth/twitter/callback');
- $_SESSION['twitter_auth'] = $request_token;
- return $twitter->getAuthorizeURL($request_token['oauth_token']);
-}
-
-$app->get('/auth/twitter', function() use($app) {
- $params = $app->request()->params();
- if($user=require_login($app, false)) {
-
- // If there is an existing Twitter token, check if it is valid
- // Otherwise, generate a Twitter login link
- $twitter_login_url = false;
- $twitter = new \TwitterOAuth\Api(Config::$twitterClientID, Config::$twitterClientSecret,
- $user->twitter_access_token, $user->twitter_token_secret);
-
- if(array_key_exists('login', $params)) {
- $twitter = new \TwitterOAuth\Api(Config::$twitterClientID, Config::$twitterClientSecret);
- $twitter_login_url = getTwitterLoginURL($twitter);
- } else {
- if($user->twitter_access_token) {
- if ($twitter->get('account/verify_credentials')) {
- $app->response()['Content-type'] = 'application/json';
- $app->response()->body(json_encode(array(
- 'result' => 'ok'
- )));
- return;
- } else {
- // If the existing twitter token is not valid, generate a login link
- $twitter_login_url = getTwitterLoginURL($twitter);
- }
- } else {
- $twitter_login_url = getTwitterLoginURL($twitter);
- }
- }
-
- $app->response()['Content-type'] = 'application/json';
- $app->response()->body(json_encode(array(
- 'url' => $twitter_login_url
- )));
-
- } else {
- $app->response()['Content-type'] = 'application/json';
- $app->response()->body(json_encode(array(
- 'result' => 'error'
- )));
- }
-});
-
-$app->get('/auth/twitter/callback', function() use($app) {
- if($user=require_login($app)) {
- $params = $app->request()->params();
-
- $twitter = new \TwitterOAuth\Api(Config::$twitterClientID, Config::$twitterClientSecret,
- $_SESSION['twitter_auth']['oauth_token'], $_SESSION['twitter_auth']['oauth_token_secret']);
- $credentials = $twitter->getAccessToken($params['oauth_verifier']);
-
- $user->twitter_access_token = $credentials['oauth_token'];
- $user->twitter_token_secret = $credentials['oauth_token_secret'];
- $user->twitter_username = $credentials['screen_name'];
- $user->save();
-
- $app->redirect('/settings');
- }
-});
-
-$app->get('/auth/instagram', function() use($app) {
- if($user=require_login($app, false)) {
-
- $instagram = instagram_client();
-
- // If there is an existing Instagram auth token, check if it's valid
- if($user->instagram_access_token) {
- $instagram->setAccessToken($user->instagram_access_token);
- $igUser = $instagram->getUser();
-
- if($igUser && $igUser->meta->code == 200) {
- $app->response()['Content-type'] = 'application/json';
- $app->response()->body(json_encode(array(
- 'result' => 'ok',
- 'username' => $igUser->data->username,
- 'url' => $instagram->getLoginUrl(array('basic','likes'))
- )));
- return;
- }
- }
-
- $app->response()['Content-type'] = 'application/json';
- $app->response()->body(json_encode(array(
- 'result' => 'error',
- 'url' => $instagram->getLoginUrl(array('basic','likes'))
- )));
- }
-});
-
-$app->get('/auth/instagram/callback', function() use($app) {
- if($user=require_login($app)) {
- $params = $app->request()->params();
-
- $instagram = instagram_client();
- $data = $instagram->getOAuthToken($params['code']);
- $user->instagram_access_token = $data->access_token;
- $user->save();
-
- $app->redirect('/settings');
- }
-});
diff --git a/lib/helpers.php b/lib/helpers.php
index cb9edd0..81ad38b 100644
--- a/lib/helpers.php
+++ b/lib/helpers.php
@@ -97,8 +97,9 @@ function micropub_post_for_user(&$user, $params, $file_path = NULL, $json = fals
$user->last_micropub_response_date = date('Y-m-d H:i:s');
// Check the response and look for a "Location" header containing the URL
- if($r['response'] && preg_match('/Location: (.+)/', $r['response'], $match)) {
- $r['location'] = trim($match[1]);
+ if($r['response'] && ($r['code'] == 201 || $r['code'] == 202)
+ && isset($r['headers']['Location'])) {
+ $r['location'] = $r['headers']['Location'][0];
$user->micropub_success = 1;
} else {
$r['location'] = false;
@@ -168,10 +169,16 @@ function micropub_post($endpoint, $params, $access_token, $file_path = NULL, $js
$response = curl_exec($ch);
$error = curl_error($ch);
$sent_headers = curl_getinfo($ch, CURLINFO_HEADER_OUT);
+
+ $header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
+ $header_str = trim(substr($response, 0, $header_size));
+
$request = $sent_headers . (is_string($post) ? $post : http_build_query($post));
return array(
'request' => $request,
'response' => $response,
+ 'code' => curl_getinfo($ch, CURLINFO_HTTP_CODE),
+ 'headers' => parse_headers($header_str),
'error' => $error,
'curlinfo' => curl_getinfo($ch)
);
@@ -207,6 +214,28 @@ function micropub_get($endpoint, $params, $access_token) {
);
}
+function parse_headers($headers) {
+ $retVal = array();
+ $fields = explode("\r\n", preg_replace('/\x0D\x0A[\x09\x20]+/', ' ', $headers));
+ foreach($fields as $field) {
+ if(preg_match('/([^:]+): (.+)/m', $field, $match)) {
+ $match[1] = preg_replace_callback('/(?<=^|[\x09\x20\x2D])./', function($m) {
+ return strtoupper($m[0]);
+ }, strtolower(trim($match[1])));
+ // If there's already a value set for the header name being returned, turn it into an array and add the new value
+ $match[1] = preg_replace_callback('/(?<=^|[\x09\x20\x2D])./', function($m) {
+ return strtoupper($m[0]);
+ }, strtolower(trim($match[1])));
+ if(isset($retVal[$match[1]])) {
+ $retVal[$match[1]][] = trim($match[2]);
+ } else {
+ $retVal[$match[1]] = [trim($match[2])];
+ }
+ }
+ }
+ return $retVal;
+}
+
function get_micropub_config(&$user, $query=[]) {
$targets = [];
diff --git a/public/js/script.js b/public/js/script.js
index 50496d3..f6f3a48 100644
--- a/public/js/script.js
+++ b/public/js/script.js
@@ -39,3 +39,11 @@ $(function(){
});
})
+
+function auto_prefix_url_field(field) {
+ var str = field.value;
+ if(!/^https?:\/\//.test(str)) {
+ str = "http://" + str;
+ }
+ field.value = str;
+}
diff --git a/views/dashboard.php b/views/dashboard.php
index a20a44b..59cd41e 100644
--- a/views/dashboard.php
+++ b/views/dashboard.php
@@ -2,21 +2,23 @@
<?= partial('partials/header') ?>
<ul class="post-type-icons">
- <li><a href="/editor"><img src="/images/quill.svg" width="60"></a></li>
- <li><a href="/new"><img src="/images/note.svg" width="60"></a></li>
- <li><a href="/event"><img src="/images/calendar.svg" width="60"></a></li>
- <li><a href="/bookmark"><img src="/images/bookmark.svg" width="60"></a></li>
- <li><a href="/favorite"><img src="/images/star.svg" width="60"></a></li>
- <li><a href="/repost"><img src="/images/repost.svg" width="60"></a></li>
- <li><a href="/itinerary"><img src="/images/plane.svg" width="60"></a></li>
- <li><a href="/email"><img src="/images/email.svg" width="60"></a></li>
+ <li><a href="/editor">📄</a></li>
+ <li><a href="/new">✏️</a></li>
+ <li><a href="/event">📅</a></li>
+ <li><a href="/bookmark">🔖</a></li>
+ <li><a href="/favorite">👍</a></li>
+ <li><a href="/repost">♺</a></li>
+ <li><a href="/itinerary">✈️</a></li>
+ <li><a href="/review">⭐️</a></li>
+ <li><a href="/email">✉️</a></li>
</ul>
</div>
<style type="text/css">
.post-type-icons {
- margin-top: 1em;
+ margin-top: 0;
list-style-type: none;
+ font-size: 42pt;
}
.post-type-icons li {
float: left;
diff --git a/views/index.php b/views/index.php
index 3ed34c2..4e1ffb4 100644
--- a/views/index.php
+++ b/views/index.php
@@ -5,14 +5,19 @@
<p class="tagline">Quill is a simple app for posting text notes to your website.</p>
- <p>To use Quill, sign in with your domain. Your website will need to support <a href="http://indiewebcamp.com/micropub">Micropub</a> for creating new posts.</p>
+ <? if(session('me')): ?>
+ <p>You're already signed in!<p>
+ <p><a href="/dashboard" class="btn btn-primary">Continue</a></p>
+ <? else: ?>
+ <p>To use Quill, sign in with your domain. Your website will need to support <a href="https://indieweb.org/micropub">Micropub</a> for creating new posts.</p>
- <form action="/auth/start" method="get" class="form-inline">
- <input type="url" name="me" placeholder="http://example.com" value="" class="form-control">
- <input type="submit" value="Sign In" class="btn btn-primary">
- <input type="hidden" name="client_id" value="https://quill.p3k.io">
- <input type="hidden" name="redirect_uri" value="https://quill.p3k.io/auth/callback">
- </form>
+ <form action="/auth/start" method="get" class="form-inline">
+ <input type="url" name="me" placeholder="https://example.com" value="" class="form-control" onchange="auto_prefix_url_field(this)" autofocus>
+ <input type="submit" value="Sign In" class="btn btn-primary">
+ <input type="hidden" name="client_id" value="https://quill.p3k.io">
+ <input type="hidden" name="redirect_uri" value="https://quill.p3k.io/auth/callback">
+ </form>
+ <? endif; ?>
</div>
diff --git a/views/new-bookmark.php b/views/new-bookmark.php
index 417c86d..1ac208c 100644
--- a/views/new-bookmark.php
+++ b/views/new-bookmark.php
@@ -13,27 +13,27 @@
<form role="form" style="margin-top: 20px;" id="note_form">
<div class="form-group">
- <label for="note_bookmark"><code>bookmark-of</code></label>
+ <label for="note_bookmark">Bookmark URL (<code>bookmark-of</code>)</label>
<input type="text" id="note_bookmark" value="<?= $this->bookmark_url ?>" class="form-control">
</div>
<div class="form-group">
- <label for="note_name"><code>name</code></label>
+ <label for="note_name">Name (<code>name</code>)</label>
<input type="text" id="note_name" value="<?= $this->bookmark_name ?>" class="form-control">
</div>
<div class="form-group">
- <label for="note_content"><code>content</code> (optional)</label>
+ <label for="note_content">Content (<code>content</code>, optional)</label>
<textarea id="note_content" value="" class="form-control" style="height: 5em;"><?= $this->bookmark_content ?></textarea>
</div>
<div class="form-group">
- <label for="note_category"><code>category</code> (optional, comma-separated list of tags)</label>
+ <label for="note_category">Tags (<code>category</code>, optional, comma-separated list of tags)</label>
<input type="text" id="note_category" value="<?= $this->bookmark_tags ?>" class="form-control" placeholder="e.g. web, personal">
</div>
<div class="form-group">
- <label for="note_syndicate-to"><code>syndicate-to</code> <a href="javascript:reload_syndications()">(refresh)</a></label>
+ <label for="note_syndicate-to">Syndicate (<code>syndicate-to</code>) <a href="javascript:reload_syndications()">refresh</a></label>
<div id="syndication-container">
<?php
if($this->syndication_targets) {
diff --git a/views/new-post.php b/views/new-post.php
index 0d79767..21571ec 100644
--- a/views/new-post.php
+++ b/views/new-post.php
@@ -27,6 +27,8 @@
<div class="form-group">
<label for="note_photo"><code>photo</code></label>
<input type="file" name="note_photo" id="note_photo" accept="image/*">
+ <a href="javascript:switchToManualPhotoURL();" id="note_manual_photo">enter photo url</a>
+ <a href="javascript:addPhotoURL();" class="hidden" id="add_photo">add photo</a>
<br>
<div id="photo_preview_container" class="hidden">
<img src="" id="photo_preview" style="max-width: 300px; max-height: 300px;">
@@ -180,11 +182,30 @@ function restoreNoteState() {
}
function replacePhotoWithPhotoURL(url) {
- $("#note_photo").after('<input type="url" name="note_photo_url" id="note_photo_url" value="" class="form-control">');
- $("#note_photo_url").val(url);
+ $("#note_photo").after('<input type="url" name="note_photo_url[]" value="" class="note_photo_url form-control">');
+ $(".note_photo_url").val(url);
$("#note_photo").remove();
$("#photo_preview").attr("src", url);
$("#photo_preview_container").removeClass("hidden");
+ $("#note_manual_photo").addClass("hidden");
+}
+
+function switchToManualPhotoURL() {
+ $("#note_photo").after('<input type="url" name="note_photo_url[]" value="" class="note_photo_url form-control">');
+ $("#note_photo").remove();
+ $("#note_photo_url").change(function(){
+ $("#photo_preview").attr("src", $(this).val());
+ $("#photo_preview_container").removeClass("hidden");
+ });
+ $("#note_manual_photo").addClass("hidden");
+ $("#add_photo").removeClass("hidden");
+}
+
+function addPhotoURL() {
+ $(".note_photo_url:last").after('<input type="url" name="note_photo_url[]" value="" class="note_photo_url form-control" style="margin-top:2px;">');
+ if($(".note_photo_url").length == 4) {
+ $("#add_photo").remove();
+ }
}
$(function(){
@@ -193,7 +214,7 @@ $(function(){
var hasMediaEndpoint = <?= $this->media_endpoint ? 'true' : 'false' ?>;
- $("#note_content, #note_category, #note_in_reply_to, #note_slug").on('keyup change', function(e){
+ $("#note_content, #note_category, #note_in_reply_to, #note_slug, #note_photo_url").on('keyup change', function(e){
saveNoteState();
})
@@ -233,8 +254,10 @@ $(function(){
});
$("#remove_photo").on("click", function(){
$("#note_photo").val("");
+ $(".note_photo_url").val("");
$("#photo_preview").attr("src", "" );
$("#photo_preview_container").addClass("hidden");
+ saveNoteState();
});
$("#note_content").on('change keyup', function(e){
@@ -307,8 +330,12 @@ $(function(){
// Add either the photo as a file, or the photo URL depending on whether the user has a media endpoint
if(document.getElementById("note_photo") && document.getElementById("note_photo").files[0]) {
formData.append("photo", document.getElementById("note_photo").files[0]);
- } else if($("#note_photo_url").val()) {
- formData.append("photo", $("#note_photo_url").val());
+ } else if($(".note_photo_url").val()) {
+ $(".note_photo_url").each(function(){
+ if($(this).val()) {
+ formData.append("photo[]", $(this).val());
+ }
+ });
}
// Need to append a placeholder field because if the file size max is hit, $_POST will
@@ -316,12 +343,11 @@ $(function(){
// This will be stripped by Quill before it's sent to the Micropub endpoint
formData.append("null","null");
-
var request = new XMLHttpRequest();
request.open("POST", "/micropub/multipart");
request.onreadystatechange = function() {
if(request.readyState == XMLHttpRequest.DONE) {
- console.log(request.responseText);
+ // console.log(request.responseText);
try {
var response = JSON.parse(request.responseText);
localforage.removeItem('current-note');
diff --git a/views/review.php b/views/review.php
new file mode 100644
index 0000000..dacc797
--- /dev/null
+++ b/views/review.php
@@ -0,0 +1,257 @@
+<div class="narrow">
+ <?= partial('partials/header') ?>
+
+ <div style="clear: both;">
+ <div class="alert alert-success hidden" id="post_success"><strong>Success! Your post should be on your website now!</strong><br><a href="" id="post_href">View your post</a></div>
+ <div class="alert alert-danger hidden" id="post_error"><strong>There was a problem saving your post. Your endpoint did not return a Location header.</strong><br>See <a href="/creating-a-micropub-endpoint">Creating a Micropub Endpoint</a> for more information.</div>
+ </div>
+
+ <form role="form" style="margin-top: 20px;" id="note_form">
+
+ <h2>Product</h2>
+
+ <div class="row">
+ <div class="col-xs-6">
+ <div class="form-group">
+ <label>Name</label>
+ <input type="text" class="form-control" id="item_name" placeholder="" value="">
+ </div>
+ </div>
+ <div class="col-xs-6">
+ <div class="form-group">
+ <label>URL</label>
+ <input type="url" class="form-control" id="item_url" placeholder="" value="">
+ </div>
+ </div>
+ </div>
+
+ <h2>Review</h2>
+
+ <div class="rating-stars">
+ <a href="" data-rating="1"></a><a href="" data-rating="2"></a><a href="" data-rating="3"></a><a href="" data-rating="4"></a><a href="" data-rating="5"></a>
+ <span class="description">It's okay</span>
+ </div>
+
+ <div class="row review-content hidden">
+ <div class="col-xs-12">
+ <div class="form-group">
+ <textarea id="review_content" value="" class="form-control" style="height: 4em;" placeholder="Write your review here"></textarea>
+ <div id="review-html-note">
+ <input type="checkbox" id="review_is_html" value="1"> Post as HTML
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class="row review-summary hidden">
+ <div class="col-xs-12">
+ <div class="form-group">
+ <input id="review_summary" value="" class="form-control" placeholder="Review summary">
+ </div>
+ </div>
+ </div>
+
+ <div class="row review-save hidden">
+ <div class="col-xs-12">
+ <div style="float: right; margin-top: 6px;">
+ <button class="btn btn-success" id="btn_post">Post Review</button>
+ </div>
+ </div>
+ </div>
+
+ </form>
+
+</div>
+<style type="text/css">
+.alert {
+ margin-top: 1em;
+}
+.rating-stars {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+}
+.rating-stars .description {
+ display: none;
+ font-weight: bold;
+ margin-left: 20px;
+}
+.rating-stars .description.visible {
+ display: inline-block;
+}
+.rating-stars a {
+ display: inline-block;
+ width: 64px;
+ height: 64px;
+ background-repeat: no-repeat;
+ background-image: url("data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIGlkPSJMYXllcl8xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHg9IjBweCIgeT0iMHB4IgogICB3aWR0aD0iNjRweCIgaGVpZ2h0PSI2NHB4IiB2aWV3Qm94PSIwIDAgNjQgNjQiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDY0IDY0IiB4bWw6c3BhY2U9InByZXNlcnZlIj48cG9seWdvbiBmaWxsPSJub25lIiBzdHJva2U9IiNBN0E5QUMiIHN0cm9rZS13aWR0aD0iNCIgc3Ryb2tlLW1pdGVybGltaXQ9IjEwIiBwb2ludHM9IjMxLjg2Niw2LjYxOCA0MC4wOSwyMy4yODEgNTguNDc5LDI1Ljk1MyA0NS4xNzIsMzguOTIzIDQ4LjMxMyw1Ny4yMzkgMzEuODY2LDQ4LjU5MiAxNS40MTgsNTcuMjM5IDE4LjU2LDM4LjkyMyA1LjI1MywyNS45NTMgMjMuNjQyLDIzLjI4MSAiLz48L3N2Zz4=");
+
+}
+.rating-stars a.hover {
+ background-image: url("data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeD0iMHB4IiB5PSIwcHgiCiAgIHdpZHRoPSI2NHB4IiBoZWlnaHQ9IjY0cHgiIHZpZXdCb3g9IjAgMCA2NCA2NCIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgNjQgNjQiIHhtbDpzcGFjZT0icHJlc2VydmUiPjxwb2x5Z29uIGZpbGw9IiM1MUFFQ0QiIHN0cm9rZT0iIzUxQUVDRCIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2UtbWl0ZXJsaW1pdD0iMTAiIHBvaW50cz0iMzEuODY2LDYuNjE4IDQwLjA5LDIzLjI4MSA1OC40NzksMjUuOTUzIDQ1LjE3MiwzOC45MjMgNDguMzEzLDU3LjIzOSAzMS44NjYsNDguNTkyIDE1LjQxOCw1Ny4yMzkgMTguNTYsMzguOTIzIDUuMjUzLDI1Ljk1MyAyMy42NDIsMjMuMjgxICIvPjwvc3ZnPgo=");
+}
+.rating-stars a.selected {
+ background-image: url("data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIGlkPSJMYXllcl8xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHg9IjBweCIgeT0iMHB4IgogICB3aWR0aD0iNjRweCIgaGVpZ2h0PSI2NHB4IiB2aWV3Qm94PSIwIDAgNjQgNjQiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDY0IDY0IiB4bWw6c3BhY2U9InByZXNlcnZlIj48cG9seWdvbiBmaWxsPSIjRkVDMjBGIiBzdHJva2U9IiNGRUMyMEYiIHN0cm9rZS13aWR0aD0iNCIgc3Ryb2tlLW1pdGVybGltaXQ9IjEwIiBwb2ludHM9IjMxLjg2Niw2LjYxOCA0MC4wOSwyMy4yODEgNTguNDc5LDI1Ljk1MyA0NS4xNzIsMzguOTIzIDQ4LjMxMyw1Ny4yMzkgMzEuODY2LDQ4LjU5MiAxNS40MTgsNTcuMjM5IDE4LjU2LDM4LjkyMyA1LjI1MywyNS45NTMgMjMuNjQyLDIzLjI4MSAiLz48L3N2Zz4=");
+}
+
+.review-content {
+ margin-top: 1em;
+}
+#review-html-note {
+ font-size: 12px;
+ text-align: right;
+}
+
+</style>
+<script>
+var selectedRating = 0;
+var userSelectedHTML = null;
+
+function isHTML(str) {
+ var doc = new DOMParser().parseFromString(str, "text/html");
+ return Array.from(doc.body.childNodes).some(node => node.nodeType === 1);
+}
+
+function isTouchDevice() {
+ return 'ontouchstart' in document.documentElement;
+}
+
+function setSaveButtonState() {
+ if(selectedRating > 0 && $("#item_name").val() != "" && $("#item_url").val() != "") {
+ $(".review-save").removeClass("hidden");
+ } else {
+ $(".review-save").addClass("hidden");
+ }
+}
+
+$(function(){
+
+ $(".rating-stars a").on("mouseover",function(){
+ // Disable hover effects on touch devices
+ if(isTouchDevice()) { return; }
+
+ $(this).addClass("hover");
+ var to = intval($(this).data("rating"));
+ $(".rating-stars a").removeClass("selected");
+ for(var i=1; i<=to; i++) {
+ $(".rating-stars a[data-rating="+i+"]").addClass("hover").removeClass("selected");
+ }
+ var description;
+ switch(to) {
+ case 1:
+ description = "I hate it"; break;
+ case 2:
+ description = "I don't like it"; break;
+ case 3:
+ description = "It's okay"; break;
+ case 4:
+ description = "I like it"; break;
+ case 5:
+ description = "I love it!"; break;
+ }
+ $(".rating-stars .description").text(description);
+ $(".rating-stars span").addClass("visible");
+ });
+ $(".rating-stars a").on("mouseout",function(){
+ $(this).removeClass("hover");
+ });
+ $(".rating-stars").on("mouseout",function(){
+ $(".rating-stars span").removeClass("visible");
+ $(".rating-stars a").removeClass("hover");
+ if(selectedRating) {
+ for(var i=1; i<=selectedRating; i++) {
+ $(".rating-stars a[data-rating="+i+"]").addClass("selected")
+ }
+ }
+ });
+ $(".rating-stars a").on("click",function(){
+ selectedRating = intval($(this).data("rating"));
+ $(".rating-stars a").removeClass("hover").removeClass("selected");
+ for(var i=1; i<=selectedRating; i++) {
+ $(".rating-stars a[data-rating="+i+"]").addClass("selected")
+ }
+ $(".review-content").removeClass("hidden");
+ setSaveButtonState();
+ return false;
+ });
+
+ $("#review_is_html").on("click", function(){
+ if($(this).attr("checked") == "checked") {
+ userSelectedHTML = 1;
+ } else {
+ userSelectedHTML = -1;
+ }
+ });
+ $("#review_content").on("keyup", function(){
+ if(userSelectedHTML == null) {
+ if(isHTML($(this).val())) {
+ $("#review_is_html").attr("checked", "checked");
+ } else {
+ $("#review_is_html").removeAttr("checked");
+ }
+ }
+ if($(this).val() != "") {
+ $(".review-summary").removeClass("hidden");
+ } else {
+ $(".review-summary").addClass("hidden");
+ }
+
+ var scrollHeight = document.getElementById("review_content").scrollHeight;
+ var currentHeight = parseInt($("#review_content").css("height"));
+ if(Math.abs(scrollHeight - currentHeight) > 20) {
+ $("#review_content").css("height", (scrollHeight+30)+"px");
+ }
+ });
+
+ $("#item_name").on("keyup", setSaveButtonState);
+ $("#item_url").on("keyup", setSaveButtonState);
+
+ $("#btn_post").click(function(){
+ $("#btn_post").addClass("loading disabled").text("Working...");
+
+ var review = {
+ item: [{
+ type: "h-product",
+ properties: {
+ name: [$("#item_name").val()],
+ url: [$("#item_url").val()]
+ }
+ }],
+ rating: [selectedRating],
+ };
+ if($("#review_content").val() != "") {
+ if($("#review_is_html").attr("checked") == "checked") {
+ review["content"] = [{html: $("#review_content").val()}];
+ } else {
+ review["content"] = [$("#review_content").val()];
+ }
+ }
+ if($("#review_summary").val() != "") {
+ review["summary"] = [$("#review_summary").val()];
+ }
+
+ $.post("/micropub/postjson", {
+ data: JSON.stringify({
+ "type": "h-review",
+ "properties": review
+ })
+ }, function(response){
+ $("#btn_post").removeClass("loading disabled").text("Post Review");
+
+ if(response.location != false) {
+ $("#post_success").removeClass('hidden');
+ $("#post_error").addClass('hidden');
+ $("#post_href").attr("href", response.location);
+ $("#note_form").addClass("hidden");
+ } else {
+ $("#post_success").addClass('hidden');
+ $("#post_error").removeClass('hidden');
+ }
+
+ });
+ return false;
+
+ });
+
+});
+</script>