Using IndexedDB as an in-browser cache. Am I doing it right? Announcing the arrival of Valued Associate #679: Cesar Manara Planned maintenance scheduled April 17/18, 2019 at 00:00UTC (8:00pm US/Eastern)
What would be the ideal power source for a cybernetic eye?
Denied boarding although I have proper visa and documentation. To whom should I make a complaint?
Coloring maths inside a tcolorbox
How come Sam didn't become Lord of Horn Hill?
Why did the Falcon Heavy center core fall off the ASDS OCISLY barge?
Why am I getting the error "non-boolean type specified in a context where a condition is expected" for this request?
String `!23` is replaced with `docker` in command line
How to tell that you are a giant?
What exactly is a "Meth" in Altered Carbon?
2001: A Space Odyssey's use of the song "Daisy Bell" (Bicycle Built for Two); life imitates art or vice-versa?
Short Story with Cinderella as a Voo-doo Witch
If a contract sometimes uses the wrong name, is it still valid?
Fundamental Solution of the Pell Equation
What is the role of the transistor and diode in a soft start circuit?
What's the purpose of writing one's academic biography in the third person?
How can I make names more distinctive without making them longer?
Book where humans were engineered with genes from animal species to survive hostile planets
What is the meaning of the new sigil in Game of Thrones Season 8 intro?
Why was the term "discrete" used in discrete logarithm?
How to deal with a team lead who never gives me credit?
Why aren't air breathing engines used as small first stages
Is pollution the main cause of Notre Dame Cathedral's deterioration?
What does the word "veer" mean here?
Using et al. for a last / senior author rather than for a first author
Using IndexedDB as an in-browser cache. Am I doing it right?
Announcing the arrival of Valued Associate #679: Cesar Manara
Planned maintenance scheduled April 17/18, 2019 at 00:00UTC (8:00pm US/Eastern)
.everyoneloves__top-leaderboard:empty,.everyoneloves__mid-leaderboard:empty,.everyoneloves__bot-mid-leaderboard:empty margin-bottom:0;
$begingroup$
In my site:
- Users have many Activities
- Each Activity has encoded_polyline data
- I display these encoded_polylines on a map
I want to use IndexedDB (via Dexie) as an in-browser cache so that they don't need to re-download their full Activity set every time they view their map. I've never used IndexedDB before, so I don't know if I'm doing anything silly or overlooking any edge cases.
Here's a high-level description of what I think the overall process is:
- Figure out what exists on the server
- Remove anything that is present in IndexedDB but is not present on the server
- Figure out what exists in IndexedDB
- Request only the data missing in IndexedDB
- Store the new data in IndexedDB
- Query all of the data out of IndexedDB
Throughout all of this, we need to be focusing on this user. A person might view many people's pages, and therefore have a copy of many people's data in IndexedDB. So the queries to the server and IndexedDB need to be aware of which User ID is being referenced.
Here's the English Language version of what I decided to do:
- Collect all of this User's Activty IDs from the server
- Remove anything in IndexedDB that shouldn't be there (stuff deleted from the site that might still exist in IndexedDB)
- Collect all of this User's Activty IDs from IndexedDB
- Filter out anything that's present in IndexedDB and the server
- If there are no new encoded_polylines to retrieve then
putItemsFromIndexeddbOnMap
(described below) - If there are new encoded_polylines to retrieve: retrieve those from the server, then store them in IndexedDB, then
putItemsFromIndexeddbOnMap
For putItemsFromIndexeddbOnMap:
- Get all of this user's encoded_polylines from IndexedDB
- Push that data into an array
- Display that array of data on the map
Here's the JavaScript code that does what I've explained above (with some ERB because this JavaScript is embedded in a Rails view):
var db = new Dexie("db_name");
db.version(1).stores( activities: "id,user_id" );
db.open();
// get this user's activity IDs from the server
fetch('/users/' + <%= @user.id %> + '/activity_ids.json', credentials: 'same-origin'
).then(response => return response.json()
).then(activityIdsJson =>
// remove items from IndexedDB for this user that are not in activityIdsJson
// this keeps data that was deleted in the site from sticking around in IndexedDB
db.activities
.where('id')
.noneOf(activityIdsJson)
.and(function(item) return item.user_id === <%= @user.id %> )
.keys()
.then(removeActivityIds =>
db.activities.bulkDelete(removeActivityIds);
);
// get this user's existing activity IDs out of IndexedDB
db.activities.where(user_id: <%= @user.id %>).primaryKeys(function(primaryKeys)
// filter out the activity IDs that are already in IndexedDB
var neededIds = activityIdsJson.filter((id) => !primaryKeys.includes(id));
if(Array.isArray(neededIds) && neededIds.length === 0)
// we do not need to request any new data so query IndexedDB directly
putItemsFromIndexeddbOnMap();
else if(Array.isArray(neededIds))
if(neededIds.equals(activityIdsJson))
// we need all data so do not pass the `only` param
neededIds = [];
// get new data (encoded_polylines for display on the map)
fetch('/users/' + <%= @user.id %> + '/encoded_polylines.json?only=' + neededIds, credentials: 'same-origin'
).then(response => return response.json()
).then(newEncodedPolylinesJson =>
// store the new encoded_polylines in IndexedDB
db.activities.bulkPut(newEncodedPolylinesJson).then(_unused =>
// pull all encoded_polylines out of IndexedDB
putItemsFromIndexeddbOnMap();
);
);
);
);
function putItemsFromIndexeddbOnMap()
var featureCollection = [];
db.activities.where(user_id: <%= @user.id %>).each(activity =>
featureCollection.push(
type: 'Feature',
geometry: polyline.toGeoJSON(activity['encoded_polyline'])
);
).then(function()
// if there are any polylines, add them to the map
if(featureCollection.length > 0)
if(map.isStyleLoaded())
// map has fully loaded so add polylines to the map
addPolylineLayer(featureCollection);
else
// map is still loading, so wait for that to complete
map.on('style.load', addPolylineLayer(featureCollection));
).catch(error => error);
);
function addPolylineLayer(data)
map.addSource('polylineCollection',
type: 'geojson',
data:
type: 'FeatureCollection',
features: data
);
map.addLayer(
id: 'polylineCollection',
type: 'line',
source: 'polylineCollection'
);
...Am I doing it right?
javascript
$endgroup$
add a comment |
$begingroup$
In my site:
- Users have many Activities
- Each Activity has encoded_polyline data
- I display these encoded_polylines on a map
I want to use IndexedDB (via Dexie) as an in-browser cache so that they don't need to re-download their full Activity set every time they view their map. I've never used IndexedDB before, so I don't know if I'm doing anything silly or overlooking any edge cases.
Here's a high-level description of what I think the overall process is:
- Figure out what exists on the server
- Remove anything that is present in IndexedDB but is not present on the server
- Figure out what exists in IndexedDB
- Request only the data missing in IndexedDB
- Store the new data in IndexedDB
- Query all of the data out of IndexedDB
Throughout all of this, we need to be focusing on this user. A person might view many people's pages, and therefore have a copy of many people's data in IndexedDB. So the queries to the server and IndexedDB need to be aware of which User ID is being referenced.
Here's the English Language version of what I decided to do:
- Collect all of this User's Activty IDs from the server
- Remove anything in IndexedDB that shouldn't be there (stuff deleted from the site that might still exist in IndexedDB)
- Collect all of this User's Activty IDs from IndexedDB
- Filter out anything that's present in IndexedDB and the server
- If there are no new encoded_polylines to retrieve then
putItemsFromIndexeddbOnMap
(described below) - If there are new encoded_polylines to retrieve: retrieve those from the server, then store them in IndexedDB, then
putItemsFromIndexeddbOnMap
For putItemsFromIndexeddbOnMap:
- Get all of this user's encoded_polylines from IndexedDB
- Push that data into an array
- Display that array of data on the map
Here's the JavaScript code that does what I've explained above (with some ERB because this JavaScript is embedded in a Rails view):
var db = new Dexie("db_name");
db.version(1).stores( activities: "id,user_id" );
db.open();
// get this user's activity IDs from the server
fetch('/users/' + <%= @user.id %> + '/activity_ids.json', credentials: 'same-origin'
).then(response => return response.json()
).then(activityIdsJson =>
// remove items from IndexedDB for this user that are not in activityIdsJson
// this keeps data that was deleted in the site from sticking around in IndexedDB
db.activities
.where('id')
.noneOf(activityIdsJson)
.and(function(item) return item.user_id === <%= @user.id %> )
.keys()
.then(removeActivityIds =>
db.activities.bulkDelete(removeActivityIds);
);
// get this user's existing activity IDs out of IndexedDB
db.activities.where(user_id: <%= @user.id %>).primaryKeys(function(primaryKeys)
// filter out the activity IDs that are already in IndexedDB
var neededIds = activityIdsJson.filter((id) => !primaryKeys.includes(id));
if(Array.isArray(neededIds) && neededIds.length === 0)
// we do not need to request any new data so query IndexedDB directly
putItemsFromIndexeddbOnMap();
else if(Array.isArray(neededIds))
if(neededIds.equals(activityIdsJson))
// we need all data so do not pass the `only` param
neededIds = [];
// get new data (encoded_polylines for display on the map)
fetch('/users/' + <%= @user.id %> + '/encoded_polylines.json?only=' + neededIds, credentials: 'same-origin'
).then(response => return response.json()
).then(newEncodedPolylinesJson =>
// store the new encoded_polylines in IndexedDB
db.activities.bulkPut(newEncodedPolylinesJson).then(_unused =>
// pull all encoded_polylines out of IndexedDB
putItemsFromIndexeddbOnMap();
);
);
);
);
function putItemsFromIndexeddbOnMap()
var featureCollection = [];
db.activities.where(user_id: <%= @user.id %>).each(activity =>
featureCollection.push(
type: 'Feature',
geometry: polyline.toGeoJSON(activity['encoded_polyline'])
);
).then(function()
// if there are any polylines, add them to the map
if(featureCollection.length > 0)
if(map.isStyleLoaded())
// map has fully loaded so add polylines to the map
addPolylineLayer(featureCollection);
else
// map is still loading, so wait for that to complete
map.on('style.load', addPolylineLayer(featureCollection));
).catch(error => error);
);
function addPolylineLayer(data)
map.addSource('polylineCollection',
type: 'geojson',
data:
type: 'FeatureCollection',
features: data
);
map.addLayer(
id: 'polylineCollection',
type: 'line',
source: 'polylineCollection'
);
...Am I doing it right?
javascript
$endgroup$
add a comment |
$begingroup$
In my site:
- Users have many Activities
- Each Activity has encoded_polyline data
- I display these encoded_polylines on a map
I want to use IndexedDB (via Dexie) as an in-browser cache so that they don't need to re-download their full Activity set every time they view their map. I've never used IndexedDB before, so I don't know if I'm doing anything silly or overlooking any edge cases.
Here's a high-level description of what I think the overall process is:
- Figure out what exists on the server
- Remove anything that is present in IndexedDB but is not present on the server
- Figure out what exists in IndexedDB
- Request only the data missing in IndexedDB
- Store the new data in IndexedDB
- Query all of the data out of IndexedDB
Throughout all of this, we need to be focusing on this user. A person might view many people's pages, and therefore have a copy of many people's data in IndexedDB. So the queries to the server and IndexedDB need to be aware of which User ID is being referenced.
Here's the English Language version of what I decided to do:
- Collect all of this User's Activty IDs from the server
- Remove anything in IndexedDB that shouldn't be there (stuff deleted from the site that might still exist in IndexedDB)
- Collect all of this User's Activty IDs from IndexedDB
- Filter out anything that's present in IndexedDB and the server
- If there are no new encoded_polylines to retrieve then
putItemsFromIndexeddbOnMap
(described below) - If there are new encoded_polylines to retrieve: retrieve those from the server, then store them in IndexedDB, then
putItemsFromIndexeddbOnMap
For putItemsFromIndexeddbOnMap:
- Get all of this user's encoded_polylines from IndexedDB
- Push that data into an array
- Display that array of data on the map
Here's the JavaScript code that does what I've explained above (with some ERB because this JavaScript is embedded in a Rails view):
var db = new Dexie("db_name");
db.version(1).stores( activities: "id,user_id" );
db.open();
// get this user's activity IDs from the server
fetch('/users/' + <%= @user.id %> + '/activity_ids.json', credentials: 'same-origin'
).then(response => return response.json()
).then(activityIdsJson =>
// remove items from IndexedDB for this user that are not in activityIdsJson
// this keeps data that was deleted in the site from sticking around in IndexedDB
db.activities
.where('id')
.noneOf(activityIdsJson)
.and(function(item) return item.user_id === <%= @user.id %> )
.keys()
.then(removeActivityIds =>
db.activities.bulkDelete(removeActivityIds);
);
// get this user's existing activity IDs out of IndexedDB
db.activities.where(user_id: <%= @user.id %>).primaryKeys(function(primaryKeys)
// filter out the activity IDs that are already in IndexedDB
var neededIds = activityIdsJson.filter((id) => !primaryKeys.includes(id));
if(Array.isArray(neededIds) && neededIds.length === 0)
// we do not need to request any new data so query IndexedDB directly
putItemsFromIndexeddbOnMap();
else if(Array.isArray(neededIds))
if(neededIds.equals(activityIdsJson))
// we need all data so do not pass the `only` param
neededIds = [];
// get new data (encoded_polylines for display on the map)
fetch('/users/' + <%= @user.id %> + '/encoded_polylines.json?only=' + neededIds, credentials: 'same-origin'
).then(response => return response.json()
).then(newEncodedPolylinesJson =>
// store the new encoded_polylines in IndexedDB
db.activities.bulkPut(newEncodedPolylinesJson).then(_unused =>
// pull all encoded_polylines out of IndexedDB
putItemsFromIndexeddbOnMap();
);
);
);
);
function putItemsFromIndexeddbOnMap()
var featureCollection = [];
db.activities.where(user_id: <%= @user.id %>).each(activity =>
featureCollection.push(
type: 'Feature',
geometry: polyline.toGeoJSON(activity['encoded_polyline'])
);
).then(function()
// if there are any polylines, add them to the map
if(featureCollection.length > 0)
if(map.isStyleLoaded())
// map has fully loaded so add polylines to the map
addPolylineLayer(featureCollection);
else
// map is still loading, so wait for that to complete
map.on('style.load', addPolylineLayer(featureCollection));
).catch(error => error);
);
function addPolylineLayer(data)
map.addSource('polylineCollection',
type: 'geojson',
data:
type: 'FeatureCollection',
features: data
);
map.addLayer(
id: 'polylineCollection',
type: 'line',
source: 'polylineCollection'
);
...Am I doing it right?
javascript
$endgroup$
In my site:
- Users have many Activities
- Each Activity has encoded_polyline data
- I display these encoded_polylines on a map
I want to use IndexedDB (via Dexie) as an in-browser cache so that they don't need to re-download their full Activity set every time they view their map. I've never used IndexedDB before, so I don't know if I'm doing anything silly or overlooking any edge cases.
Here's a high-level description of what I think the overall process is:
- Figure out what exists on the server
- Remove anything that is present in IndexedDB but is not present on the server
- Figure out what exists in IndexedDB
- Request only the data missing in IndexedDB
- Store the new data in IndexedDB
- Query all of the data out of IndexedDB
Throughout all of this, we need to be focusing on this user. A person might view many people's pages, and therefore have a copy of many people's data in IndexedDB. So the queries to the server and IndexedDB need to be aware of which User ID is being referenced.
Here's the English Language version of what I decided to do:
- Collect all of this User's Activty IDs from the server
- Remove anything in IndexedDB that shouldn't be there (stuff deleted from the site that might still exist in IndexedDB)
- Collect all of this User's Activty IDs from IndexedDB
- Filter out anything that's present in IndexedDB and the server
- If there are no new encoded_polylines to retrieve then
putItemsFromIndexeddbOnMap
(described below) - If there are new encoded_polylines to retrieve: retrieve those from the server, then store them in IndexedDB, then
putItemsFromIndexeddbOnMap
For putItemsFromIndexeddbOnMap:
- Get all of this user's encoded_polylines from IndexedDB
- Push that data into an array
- Display that array of data on the map
Here's the JavaScript code that does what I've explained above (with some ERB because this JavaScript is embedded in a Rails view):
var db = new Dexie("db_name");
db.version(1).stores( activities: "id,user_id" );
db.open();
// get this user's activity IDs from the server
fetch('/users/' + <%= @user.id %> + '/activity_ids.json', credentials: 'same-origin'
).then(response => return response.json()
).then(activityIdsJson =>
// remove items from IndexedDB for this user that are not in activityIdsJson
// this keeps data that was deleted in the site from sticking around in IndexedDB
db.activities
.where('id')
.noneOf(activityIdsJson)
.and(function(item) return item.user_id === <%= @user.id %> )
.keys()
.then(removeActivityIds =>
db.activities.bulkDelete(removeActivityIds);
);
// get this user's existing activity IDs out of IndexedDB
db.activities.where(user_id: <%= @user.id %>).primaryKeys(function(primaryKeys)
// filter out the activity IDs that are already in IndexedDB
var neededIds = activityIdsJson.filter((id) => !primaryKeys.includes(id));
if(Array.isArray(neededIds) && neededIds.length === 0)
// we do not need to request any new data so query IndexedDB directly
putItemsFromIndexeddbOnMap();
else if(Array.isArray(neededIds))
if(neededIds.equals(activityIdsJson))
// we need all data so do not pass the `only` param
neededIds = [];
// get new data (encoded_polylines for display on the map)
fetch('/users/' + <%= @user.id %> + '/encoded_polylines.json?only=' + neededIds, credentials: 'same-origin'
).then(response => return response.json()
).then(newEncodedPolylinesJson =>
// store the new encoded_polylines in IndexedDB
db.activities.bulkPut(newEncodedPolylinesJson).then(_unused =>
// pull all encoded_polylines out of IndexedDB
putItemsFromIndexeddbOnMap();
);
);
);
);
function putItemsFromIndexeddbOnMap()
var featureCollection = [];
db.activities.where(user_id: <%= @user.id %>).each(activity =>
featureCollection.push(
type: 'Feature',
geometry: polyline.toGeoJSON(activity['encoded_polyline'])
);
).then(function()
// if there are any polylines, add them to the map
if(featureCollection.length > 0)
if(map.isStyleLoaded())
// map has fully loaded so add polylines to the map
addPolylineLayer(featureCollection);
else
// map is still loading, so wait for that to complete
map.on('style.load', addPolylineLayer(featureCollection));
).catch(error => error);
);
function addPolylineLayer(data)
map.addSource('polylineCollection',
type: 'geojson',
data:
type: 'FeatureCollection',
features: data
);
map.addLayer(
id: 'polylineCollection',
type: 'line',
source: 'polylineCollection'
);
...Am I doing it right?
javascript
javascript
asked 2 mins ago
James ChevalierJames Chevalier
1286
1286
add a comment |
add a comment |
0
active
oldest
votes
Your Answer
StackExchange.ifUsing("editor", function ()
StackExchange.using("externalEditor", function ()
StackExchange.using("snippets", function ()
StackExchange.snippets.init();
);
);
, "code-snippets");
StackExchange.ready(function()
var channelOptions =
tags: "".split(" "),
id: "196"
;
initTagRenderer("".split(" "), "".split(" "), channelOptions);
StackExchange.using("externalEditor", function()
// Have to fire editor after snippets, if snippets enabled
if (StackExchange.settings.snippets.snippetsEnabled)
StackExchange.using("snippets", function()
createEditor();
);
else
createEditor();
);
function createEditor()
StackExchange.prepareEditor(
heartbeatType: 'answer',
autoActivateHeartbeat: false,
convertImagesToLinks: false,
noModals: true,
showLowRepImageUploadWarning: true,
reputationToPostImages: null,
bindNavPrevention: true,
postfix: "",
imageUploader:
brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
allowUrls: true
,
onDemand: true,
discardSelector: ".discard-answer"
,immediatelyShowMarkdownHelp:true
);
);
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
var $window = $(window),
onScroll = function(e)
var $elem = $('.new-login-left'),
docViewTop = $window.scrollTop(),
docViewBottom = docViewTop + $window.height(),
elemTop = $elem.offset().top,
elemBottom = elemTop + $elem.height();
if ((docViewTop elemBottom))
StackExchange.using('gps', function() StackExchange.gps.track('embedded_signup_form.view', location: 'question_page' ); );
$window.unbind('scroll', onScroll);
;
$window.on('scroll', onScroll);
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function ()
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f217586%2fusing-indexeddb-as-an-in-browser-cache-am-i-doing-it-right%23new-answer', 'question_page');
);
Post as a guest
Required, but never shown
0
active
oldest
votes
0
active
oldest
votes
active
oldest
votes
active
oldest
votes
Thanks for contributing an answer to Code Review Stack Exchange!
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
Use MathJax to format equations. MathJax reference.
To learn more, see our tips on writing great answers.
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
var $window = $(window),
onScroll = function(e)
var $elem = $('.new-login-left'),
docViewTop = $window.scrollTop(),
docViewBottom = docViewTop + $window.height(),
elemTop = $elem.offset().top,
elemBottom = elemTop + $elem.height();
if ((docViewTop elemBottom))
StackExchange.using('gps', function() StackExchange.gps.track('embedded_signup_form.view', location: 'question_page' ); );
$window.unbind('scroll', onScroll);
;
$window.on('scroll', onScroll);
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function ()
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f217586%2fusing-indexeddb-as-an-in-browser-cache-am-i-doing-it-right%23new-answer', 'question_page');
);
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
var $window = $(window),
onScroll = function(e)
var $elem = $('.new-login-left'),
docViewTop = $window.scrollTop(),
docViewBottom = docViewTop + $window.height(),
elemTop = $elem.offset().top,
elemBottom = elemTop + $elem.height();
if ((docViewTop elemBottom))
StackExchange.using('gps', function() StackExchange.gps.track('embedded_signup_form.view', location: 'question_page' ); );
$window.unbind('scroll', onScroll);
;
$window.on('scroll', onScroll);
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
var $window = $(window),
onScroll = function(e)
var $elem = $('.new-login-left'),
docViewTop = $window.scrollTop(),
docViewBottom = docViewTop + $window.height(),
elemTop = $elem.offset().top,
elemBottom = elemTop + $elem.height();
if ((docViewTop elemBottom))
StackExchange.using('gps', function() StackExchange.gps.track('embedded_signup_form.view', location: 'question_page' ); );
$window.unbind('scroll', onScroll);
;
$window.on('scroll', onScroll);
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
var $window = $(window),
onScroll = function(e)
var $elem = $('.new-login-left'),
docViewTop = $window.scrollTop(),
docViewBottom = docViewTop + $window.height(),
elemTop = $elem.offset().top,
elemBottom = elemTop + $elem.height();
if ((docViewTop elemBottom))
StackExchange.using('gps', function() StackExchange.gps.track('embedded_signup_form.view', location: 'question_page' ); );
$window.unbind('scroll', onScroll);
;
$window.on('scroll', onScroll);
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown