Add tutorial 1st draft
This commit is contained in:
@@ -19,7 +19,6 @@
|
||||
"@fullhuman/postcss-purgecss": "^1.3.0",
|
||||
"autoprefixer": "^9.6.0",
|
||||
"babel-loader": "^8.0.6",
|
||||
"babel-plugin-prismjs": "^1.1.1",
|
||||
"babel-polyfill": "^6.26.0",
|
||||
"cross-env": "^6.0.3",
|
||||
"css-loader": "^2.1.1",
|
||||
|
||||
14
src/index.js
14
src/index.js
@@ -1,17 +1,7 @@
|
||||
import './style.css'
|
||||
|
||||
import Prism from 'prismjs'
|
||||
|
||||
import 'prismjs/themes/prism-coy.css'
|
||||
import 'prismjs/components/prism-bash'
|
||||
import 'prismjs/components/prism-javascript'
|
||||
|
||||
import 'prismjs/plugins/line-numbers/prism-line-numbers'
|
||||
import 'prismjs/plugins/line-numbers/prism-line-numbers.css'
|
||||
import Prism from './prism'
|
||||
|
||||
import './prism.css'
|
||||
|
||||
Prism.highlightAll();
|
||||
import './style.css'
|
||||
|
||||
window.displayMailChimpStatus = function (data) {
|
||||
if (!data.result || !data.msg) return
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<ul>
|
||||
<li><a href="#getting-started" class="font-semibold">Getting started</a></li>
|
||||
<li><a href="#sdk" class="font-semibold">SDK</a></li>
|
||||
<li><a href="https://github.com/encrypted-dev/userbase/blob/ugliest-tutorial/docs/examples/ugliest-todo-app/tutorial.md" class="font-semibold">Tutorial</a></li>
|
||||
<li><a href="#tutorial" class="font-semibold">Tutorial</a></li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
@@ -16,29 +16,35 @@
|
||||
<h2 id=>Getting Started</h2>
|
||||
|
||||
<h3>Create a free Userbase account</h3>
|
||||
<p>First you need a Userbase account. You will be able to create a free account when Userbase gets launched. No credit card required.</p>
|
||||
<p>First you need a Userbase developer account. You will be able to create a free account when Userbase gets launched. No credit card required.</p>
|
||||
|
||||
<h3>Install the SDK</h3>
|
||||
<p>Then you need to include the Userbase SDK into your web app.</p>
|
||||
<p>You can either include the SDK with a <script> tag:</p>
|
||||
|
||||
<pre>
|
||||
<code class="language-html"><script type="text/javascript"
|
||||
src="https://userbase-public.s3-us-west-2.amazonaws.com/userbase-js/userbase.js">
|
||||
</script></code>
|
||||
<code class="language-markup"><!--
|
||||
<script type="text/javascript"
|
||||
src="https://userbase-public.s3-us-west-2.amazonaws.com/userbase-js/userbase.js">
|
||||
</script>
|
||||
--></code>
|
||||
</pre>
|
||||
|
||||
<p>Or else you can include the SDK into your build pipeline:</p>
|
||||
|
||||
<pre>
|
||||
<code class="language-bash">npm --install --save userbase-js</code>
|
||||
<code class="language-bash">
|
||||
npm --install --save userbase-js
|
||||
</code>
|
||||
</pre>
|
||||
|
||||
<h3>Set the Application ID</h3>
|
||||
<p>From your Userbase account, create a new App and get the Application ID. Then you just need to configure the Userbase SDK to use it:</p>
|
||||
|
||||
<pre>
|
||||
<code class="language-bash">userbase.configure({ appId: 'a43ae910-fc89-43fe-a7a3-a11a53b49325' })</code>
|
||||
<code class="language-javascript"><!--
|
||||
userbase.configure({ appId: 'a43ae910-fc89-43fe-a7a3-a11a53b49325' })
|
||||
--></code>
|
||||
</pre>
|
||||
|
||||
<p>And you're all set.</p>
|
||||
@@ -52,7 +58,7 @@
|
||||
|
||||
<p>You can use Userbase through a simple JavaScript SDK in the browser. The following is the complete set of Userbase APIs that let you create user accounts, handle logins, and persist user data.</p>
|
||||
|
||||
<h3 id="zero-management-database">Users</h3>
|
||||
<h3 id="sdk-users">Users</h3>
|
||||
<p>Use these APIs to create user accounts, handle logins and logouts, and resume sessions when a user returns to your web app.</p>
|
||||
<ul>
|
||||
<li><a href="/docs/sdk/sign-up" class="font-semibold">SignUp</a></li>
|
||||
@@ -61,7 +67,7 @@
|
||||
<li><a href="/docs/sdk/sign-out" class="font-semibold">SignOut</a></li>
|
||||
</ul>
|
||||
|
||||
<h3 id="user-accounts">Data</h3>
|
||||
<h3 id="sdk-data">Data</h3>
|
||||
<p>Use these APIs to store and retrieve user data. All data handled by these APIs is highly-durable, immediately consistent, and end-to-end encrypted.</p>
|
||||
<ul>
|
||||
<li><a href="/docs/sdk/open-database" class="font-semibold">OpenDatabase</a></li>
|
||||
@@ -72,4 +78,823 @@
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<hr>
|
||||
|
||||
<div class="section">
|
||||
<a class="anchor" id="tutorial"></a>
|
||||
<h2>Tutorial</h2>
|
||||
|
||||
<p>
|
||||
In this tutorial we will build a simple to-do web app. Even if the web app you're
|
||||
building has nothing to do with to-dos, the techniques we'll cover can be
|
||||
applied to make many other kinds of web apps.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
With just 199 lines code inside a single static HTML file we will create an
|
||||
end-to-end encrypted web application with:
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
<li>User sign up, sign in and sign out.</li>
|
||||
<li>User data persistence.</li>
|
||||
<li>End-to-end encryption of user data.</li>
|
||||
<li>Live data synchornization across different devices.</li>
|
||||
</ul>
|
||||
|
||||
<p>You can see a live demo of what we'll building <a href="https://ugliest-todo.netlify.com/" target="_blank">here</a>.</p>
|
||||
|
||||
<h3>Prerequisites</h3>
|
||||
|
||||
<p>You'll need to be familiar with HTML and JavaScript. Beyond the <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/A_re-introduction_to_JavaScript">basics of JavaScript</a>, you will need to be familiar with <a href="https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Client-side_web_APIs/Manipulating_documents#Active_learning_Basic_DOM_manipulation">DOM manipulation</a>, <a href="https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/Events">events</a>, and <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises">Promises</a>.</p>
|
||||
|
||||
|
||||
<h3>What's in a name?</h3>
|
||||
|
||||
<p>
|
||||
The reason we are calling our application "Ugly To-Do" is because we aren't
|
||||
going to apply any styling but solely focus on core functionality. You can think
|
||||
of this tutorial as a sort of "hello world" for Userbase, a demonstration of the
|
||||
core functionality in the simplest way possible.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
In a real project you'll likely want a more sophisticated approach: for instance, you may use React to control the DOM or a module bundler to package your application from multiple files, and at the very
|
||||
least you'll want to display better error messages and add some styling.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
We are working on a collection of tutorials and sample applications that will
|
||||
show you how to do all these things with Userbase and more. You can <a href="https://userbase.dev/mailing-list">subscribe to our mailing list</a> to get updates on these and more.
|
||||
</p>
|
||||
|
||||
<h3>Setting up</h3>
|
||||
|
||||
<p>Let's get setup to build our application. Open up a new file in your favorite editor:</p>
|
||||
|
||||
<pre>
|
||||
<code class="language-bash">
|
||||
code ulgy-todo.html
|
||||
</code>
|
||||
</pre>
|
||||
|
||||
<p>And add some boilerplate HTML:</p>
|
||||
|
||||
<div>
|
||||
<pre>
|
||||
<code class="language-markup">
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Ugly To-Do</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- application code -->
|
||||
<script type="text/javascript">
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Now open up this file in a web browser of your choosing. At this point all
|
||||
you'll see is a blank page. As we add functionality throughout the tutorial, you
|
||||
can reload the app by refreshing this page to see changes.
|
||||
</p>
|
||||
|
||||
<h3>Creating a developer account</h3>
|
||||
|
||||
<p>To complete this tutorial, you'll need to create a Userbase developer account, and then create an app from within your account. Take not of the Application ID of your app.</p>
|
||||
|
||||
<p>
|
||||
We are now ready to start building the application. We'll start by implementing
|
||||
functionality to sign up and sign in users and then implement to-do
|
||||
functionality.
|
||||
</p>
|
||||
|
||||
<h3>Installing the SDK</h3>
|
||||
|
||||
To use the Userbase SDK in our app, we'll load it from a CDN with a <code class="language-markup"><script></code> tag in the head of
|
||||
our page:
|
||||
|
||||
<div>
|
||||
<pre data-line="4-6">
|
||||
<code class="language-markup"><!--
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Ugliest To-Do App</title>
|
||||
<script type="text/javascript"
|
||||
src="https://userbase-public.s3-us-west-2.amazonaws.com/userbase-js/userbase.js">
|
||||
</script>
|
||||
</head>
|
||||
--></code>
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
The Userbase SDK will now be accessible via the global <code class="language-javascript">userbase</code> variable.
|
||||
|
||||
<h3>Configuring the SDK</h3>
|
||||
|
||||
<p>
|
||||
Before doing anything with the Userbase SDK, we need to configure it with our
|
||||
Application ID (make sure to replace <code class="language-javascript">'YOUR_APP_ID'</code> with the Application ID of the Userbase App you created earlier):
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<pre data-line="8-10">
|
||||
<code class="language-markup"><!--
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Ugliest To-Do App</title>
|
||||
<script type="text/javascript"
|
||||
src="https://userbase-public.s3-us-west-2.amazonaws.com/userbase-js/userbase.js">
|
||||
</script>
|
||||
|
||||
<script type="text/javascript">
|
||||
userbase.configure({ appId: 'YOUR_APP_ID' })
|
||||
</script>
|
||||
</head>
|
||||
--></code>
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Now anything we do with the client (e.g. sign in a user, persist data) will
|
||||
happen within the context of the app whose ID we specified.
|
||||
</p>
|
||||
|
||||
<h3>Letting new users create an account</h3>
|
||||
|
||||
<p>
|
||||
Any actions that our users take will need to take place within an authenticated session.
|
||||
We'll start off by adding a way to for new users to create an account with our app.
|
||||
</p>
|
||||
|
||||
<p>First, we'll add a sign up form:</p>
|
||||
|
||||
<div>
|
||||
<pre data-line="2-11">
|
||||
<code class="language-markup">
|
||||
<body>
|
||||
<!-- Auth View -->
|
||||
<div id="auth-view">
|
||||
<h1>Create an account</h1>
|
||||
<form id="signup-form">
|
||||
<input id="create-account-username" type="email" required placeholder="Email">
|
||||
<input id="create-account-password" type="password" required placeholder="Password">
|
||||
<input type="submit" value="Create an account">
|
||||
</form>
|
||||
<div id="create-account-error"></div>
|
||||
</div>
|
||||
|
||||
<!-- application code -->
|
||||
<script type="text/javascript"></script>
|
||||
</body>
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<p>Then, we'll add code to handle the form submissions:</p>
|
||||
|
||||
<div>
|
||||
<pre data-line="3-14">
|
||||
<code class="language-markup">
|
||||
<!-- application code -->
|
||||
<script type="text/javascript">
|
||||
function handleSignUp(e) {
|
||||
e.preventDefault()
|
||||
|
||||
const username = document.getElementById('create-account-username').value
|
||||
const password = document.getElementById('create-account-password').value
|
||||
|
||||
userbase.signUp(username, password)
|
||||
.then((session) => alert('You signed up!'))
|
||||
.catch((e) => document.getElementById('create-account-error').innerHTML = e)
|
||||
}
|
||||
|
||||
document.getElementById('signup-form').addEventListener('submit', handleSignUp)
|
||||
</script>
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Whenever a submit event is triggered on our sign up form, <code class="language-javascript">handleSignUp</code> will be called.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
The first thing we do in <code class="language-javascript">handleSignUp</code> is call <code class="language-javascript">preventDefault()</code> on the
|
||||
submit event. This will prevent the page from submitting to the server.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Next we get the values of the username and password inputs and call
|
||||
<code class="language-javascript">userbase.signUp(username, password)</code> which will request a new account to be created with the Userbase service. A Promise is returned that either resolves
|
||||
with a new session object, in which case we fire an alert (for now), or rejects
|
||||
with an error, in which case we display the error message.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Go ahead and reload the web app in your browser. Enter a username and password
|
||||
in the form under "Sign Up" and submit. You'll get an alert saying "You signed
|
||||
up!".
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Now try signing up for another account using the same username and you'll see an
|
||||
error message displayed under the form (since an account already exists with
|
||||
this username).
|
||||
</p>
|
||||
|
||||
<p>
|
||||
We'll come back in a bit and change this function to do something more
|
||||
interesting than just sending an alert when an user successfully signs up.
|
||||
</p>
|
||||
|
||||
<h3>Letting users log in</h3>
|
||||
|
||||
<p>Now that users can create accounts, let's give them the ability to sign in.</p>
|
||||
|
||||
<p>
|
||||
First, we'll add a "Login" form to the page above our "Create Account"
|
||||
form:
|
||||
</p>
|
||||
|
||||
<pre data-line="4-10">
|
||||
<code class="language-markup">
|
||||
<body>
|
||||
<!-- Auth View -->
|
||||
<div id="auth-view">
|
||||
<h1>Login</h1>
|
||||
<form id="login-form">
|
||||
<input id="login-username" type="email" required placeholder="Email">
|
||||
<input id="login-password" type="password" required placeholder="Password">
|
||||
<input type="submit" value="Sign in">
|
||||
</form>
|
||||
<div id="login-error"></div>
|
||||
|
||||
<h1>Create an account</h1>
|
||||
<form id="signup-form">
|
||||
</code>
|
||||
</pre>
|
||||
|
||||
<p>Then, we'll add code to handle the form submission:</p>
|
||||
|
||||
<pre data-line="3-12">
|
||||
<code class="language-markup">
|
||||
<!-- application code -->
|
||||
<script type="text/javascript">
|
||||
function handleLogin(e) {
|
||||
e.preventDefault()
|
||||
|
||||
const username = document.getElementById('login-username').value
|
||||
const password = document.getElementById('login-password').value
|
||||
|
||||
userbase.signIn(username, password)
|
||||
.then((session) => alert('You signed in!'))
|
||||
.catch((e) => document.getElementById('login-error').innerHTML = e)
|
||||
}
|
||||
|
||||
function handleSignUp(e) {
|
||||
e.preventDefault()
|
||||
|
||||
...
|
||||
|
||||
</script>
|
||||
|
||||
</code>
|
||||
</pre>
|
||||
|
||||
<p>And finally, bind the login form within our login handler:</p>
|
||||
|
||||
<pre data-line="9">
|
||||
<code class="language-markup">
|
||||
<!-- application code -->
|
||||
<script type="text/javascript">
|
||||
|
||||
...
|
||||
|
||||
.catch((e) => document.getElementById('create-account-error').innerHTML = e)
|
||||
}
|
||||
|
||||
document.getElementById('login-form').addEventListener('submit', handleLogin)
|
||||
document.getElementById('signup-form').addEventListener('submit', handleSignUp)
|
||||
</script>
|
||||
</body>
|
||||
</code>
|
||||
</pre>
|
||||
|
||||
<p>You'll notice this looks very similar to the sign up code above.</p>
|
||||
|
||||
<p>
|
||||
We define a function, <code class="language-javascript">handleLogin</code>, to handle form submissions. The function
|
||||
prevents the default form behavior, extracts the input values from the DOM, and
|
||||
calls <code class="language-javascript">userbase.signIn(username, password)</code>. This will attempt to sign in the user with the Userbase service, handling a success with an alert and a failure
|
||||
by displaying the error.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Reload the app and you'll now see a "Sign In" form. Enter the username and
|
||||
password you used to create an account in the step above and submit the form.
|
||||
You'll get an alert saying "You signed in!"
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Try submitting the form again with incorrect credentials and you'll see an error
|
||||
message displayed under the form.
|
||||
</p>
|
||||
|
||||
<h3>Showing the to-do view</h3>
|
||||
|
||||
<p>
|
||||
After a r is signed in, we'll want to hide the authentication forms, indicate
|
||||
to the user they are logged in, and display their to-do list.
|
||||
</p>
|
||||
|
||||
<p>First, we'll add a new container to the body:</p>
|
||||
|
||||
<pre data-line="4-9">
|
||||
<code class="language-markup">
|
||||
</div>
|
||||
|
||||
<!-- To-dos View -->
|
||||
<div id="todo-view">
|
||||
<div id="username"></div>
|
||||
|
||||
<h1>To-Do List</h1>
|
||||
</div>
|
||||
|
||||
<!-- application code -->
|
||||
<script type="text/javascript">
|
||||
function handleLogin(e) {
|
||||
|
||||
...
|
||||
|
||||
</script>
|
||||
</code>
|
||||
</pre>
|
||||
|
||||
<p>Then, we'll add function to display this view and initially make it hidden:</p>
|
||||
|
||||
<div>
|
||||
<pre data-line="1-5,10">
|
||||
<code class="language-javascript">
|
||||
function showTodos(username) {
|
||||
document.getElementById('auth-view').style.display = 'none'
|
||||
document.getElementById('todo-view').style.display = 'block'
|
||||
document.getElementById('username').innerHTML = username
|
||||
}
|
||||
|
||||
document.getElementById('login-form').addEventListener('submit', handleLogin)
|
||||
document.getElementById('signup-form').addEventListener('submit', handleSignUp)
|
||||
|
||||
document.getElementById('todo-view').style.display = 'none'
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Now that we have a function to show a view for signed in users, let's change
|
||||
<code class="language-javascript">handleLogin</code> to call this function when it succeed:
|
||||
</p>
|
||||
|
||||
<pre data-line="7">
|
||||
<code class="language-javascript">
|
||||
function handleLogin(e) {
|
||||
e.preventDefault()
|
||||
|
||||
const password = document.getElementById('login-password').value
|
||||
|
||||
userbase.signIn(username, password)
|
||||
.then((session) => showTodos(session.username))
|
||||
.catch((e) => document.getElementById('login-error').innerHTML = e)
|
||||
}
|
||||
</code>
|
||||
</pre>
|
||||
|
||||
<p>And we do the same thing for <code class="language-javascript">handleSignUp</code>:</p>
|
||||
|
||||
<pre data-line="7">
|
||||
<code class="language-javascript">
|
||||
function handleSignUp(e) {
|
||||
e.preventDefault()
|
||||
|
||||
const password = document.getElementById('create-account-password').value
|
||||
|
||||
userbase.signUp(username, password)
|
||||
.then((session) => showTodos(session.username))
|
||||
.catch((e) => document.getElementById('create-account-error').innerHTML = e)
|
||||
}
|
||||
</code>
|
||||
</pre>
|
||||
|
||||
<p>
|
||||
Reload the app and sign in using your username and password. You'll see the
|
||||
authentication view disappear and your username show up along with "To-Do List".
|
||||
</p>
|
||||
|
||||
<h3>Using to the database</h3>
|
||||
|
||||
<p>
|
||||
Each time a new session is started, we need to establish a connection with the
|
||||
database that will hold that user's to-dos.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
First, let's add a couple elements for showing a loading indicator and error
|
||||
messages:
|
||||
</p>
|
||||
|
||||
<pre data-line="5-7">
|
||||
<code class="language-markup"><!--
|
||||
<div id="todo-view">
|
||||
<div id="username"></div>
|
||||
|
||||
<h1>To-Do List</h1>
|
||||
<div id="todos"></div>
|
||||
<div id="db-loading">Loading to-dos...</div>
|
||||
<div id="db-error"></div>
|
||||
</div>
|
||||
--></code>
|
||||
</pre>
|
||||
|
||||
<p>Then, we'll change <code class="language-javascript">showTodos</code> to open a new database with the Userbase service:</p>
|
||||
|
||||
<pre data-line="5,7-17,20-28">
|
||||
<code class="language-javascript">
|
||||
function showTodos(username) {
|
||||
document.getElementById('auth-view').style.display = 'none'
|
||||
document.getElementById('todo-view').style.display = 'block'
|
||||
|
||||
// reset the todos view
|
||||
document.getElementById('username').innerHTML = username
|
||||
document.getElementById('db-loading').style.display = 'block'
|
||||
document.getElementById('db-error').innerText = ''
|
||||
|
||||
userbase.openDatabase('todos', handleDatabaseChange)
|
||||
.then(() => {
|
||||
document.getElementById('db-loading').style.display = 'none'
|
||||
})
|
||||
.catch((e) => {
|
||||
document.getElementById('db-loading').style.display = 'none'
|
||||
document.getElementById('db-error').innerText = e
|
||||
})
|
||||
}
|
||||
|
||||
function handleDatabaseChange(items) {
|
||||
const todosList = document.getElementById('todos')
|
||||
|
||||
if (items.length === 0) {
|
||||
todosList.innerText = "Empty"
|
||||
} else {
|
||||
// render to-dos, not yet implemented
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('login-form').addEventListener('submit', handleLogin)
|
||||
document.getElementById('signup-form').addEventListener('submit', handleSignUp)
|
||||
</code>
|
||||
</pre>
|
||||
|
||||
<p>
|
||||
We change <code class="language-javascript">showTodos</code> to make a call to <code class="language-javascript">userbase.openDatabase('todos',
|
||||
handleDatabaseChange)</code>, <code class="language-javascript">'todos'</code> being the name of the database we want to open
|
||||
and <code class="language-javascript">handleDatabaseChange</code> being a callback for receiving changes to data in the
|
||||
database. The Userbase service will attempt to open the user's database by the
|
||||
name of <code class="language-javascript">'todos'</code> (creating it if it doesn't already exist). After the <code class="language-javascript">'todos'</code>
|
||||
database is opened, and whenever data changes in the database, our callback
|
||||
function <code class="language-javascript">handleDatabaseChanges</code> will be called. A Promise is returned that will
|
||||
either resolve if the database was successfully opened, in which case we hide
|
||||
the loading indicator, or otherwise reject, in which case we display the error
|
||||
message.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
We add a function <code class="language-javascript">handleDatabaseChange</code> for receiving changes to the database.
|
||||
We check to see if there are any items in the database and if it's empty we display
|
||||
this in the to-dos container. We'll implement the other case in the next step.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Reload the app and sign in. You'll see the "Loading to-dos..." as a connection
|
||||
to the database is established followed by "Empty" indicating there are
|
||||
currently no to-dos.
|
||||
</p>
|
||||
|
||||
|
||||
<h3>Display the to-dos</h3>
|
||||
|
||||
<p>
|
||||
If the database has items in it, we'll want to render those under to-do list.
|
||||
Let's implement that case in <code class="language-javascript">handleDatabaseChange</code>:
|
||||
</p>
|
||||
|
||||
<pre data-line="7-22">
|
||||
<code class="language-javascript"><!--
|
||||
function handleDatabaseChange(items) {
|
||||
const todosList = document.getElementById('todos')
|
||||
|
||||
if (items.length === 0) {
|
||||
todosList.innerText = "Empty"
|
||||
} else {
|
||||
// clear the list
|
||||
todosList.innerHTML = ''
|
||||
|
||||
// render all the to-do items
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
|
||||
// build the todo label
|
||||
const todoLabel = document.createElement('label')
|
||||
todoLabel.innerHTML = items[i].record.todo
|
||||
todoLabel.style.textDecoration = items[i].record.complete ? 'line-through' : 'none'
|
||||
|
||||
// append the todo item to the list
|
||||
const todoItem = document.createElement('div')
|
||||
todoItem.appendChild(todoLabel)
|
||||
todosList.appendChild(todoItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
--></code>
|
||||
</pre>
|
||||
|
||||
<h3>Adding to-dos</h3>
|
||||
|
||||
<p>Let's add a form for creating new to-dos:</p>
|
||||
|
||||
<script type="text/plain" data-line="10-14" class="language-markup">
|
||||
<!-- To-dos View -->
|
||||
<div id="todo-view">
|
||||
<div id="username"></div>
|
||||
|
||||
<h1>To-Do List</h1>
|
||||
<div id="todos"></div>
|
||||
<div id="db-loading">Loading to-dos...</div>
|
||||
<div id="db-error"></div>
|
||||
|
||||
<form id="add-todo-form">
|
||||
<input id="add-todo" type="text" required placeholder="To-Do">
|
||||
<input type="submit" value="Add">
|
||||
</form>
|
||||
<div id="add-todo-error"></div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<p>Then, add code to handle form submissions:</p>
|
||||
|
||||
<pre data-line="5-13,18">
|
||||
<code class="language-markup">
|
||||
<!-- application code -->
|
||||
<script type="text/javascript">
|
||||
...
|
||||
|
||||
function addTodoHandler(e) {
|
||||
e.preventDefault()
|
||||
|
||||
const todo = document.getElementById('add-todo').value
|
||||
|
||||
userbase.insert('todos', { 'todo': todo }, Date.now())
|
||||
.then(() => document.getElementById('add-todo').value = '')
|
||||
.catch((e) => document.getElementById('add-todo-error').innerHTML = e)
|
||||
}
|
||||
|
||||
document.getElementById('login-form').addEventListener('submit', handleLogin)
|
||||
document.getElementById('signup-form').addEventListener('submit', handleSignUp)
|
||||
|
||||
document.getElementById('add-todo-form').addEventListener('submit', addTodoHandler)
|
||||
document.getElementById('todo-view').style.display = 'none'
|
||||
|
||||
</script>
|
||||
</code>
|
||||
</pre>
|
||||
|
||||
<p>
|
||||
In <code class="language-javascript">addTodoHandler</code> we first call <code class="language-javascript">preventDefault()</code> to stop the default form
|
||||
behavior, pull the to-do text from the input, and then call <code class="language-javascript">userbase.insert</code>
|
||||
with the database name, object we want the persist, and the current time. This
|
||||
will return a Promise that will resolve if the data is successfully persisted to
|
||||
the database, in which case we clear the form input, or reject if the insert
|
||||
failed, in which case we display the error message below the form.
|
||||
</p>
|
||||
|
||||
<h3>Updating to-dos</h3>
|
||||
|
||||
<p>Let's modify how we are rendering a to-do so we can mark a to-do as completed:</p>
|
||||
|
||||
<pre data-line="4-16,26">
|
||||
<code class="language-javascript">
|
||||
// render all the to-do items
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
|
||||
// build the todo checkbox
|
||||
const todoBox = document.createElement('input')
|
||||
todoBox.type = 'checkbox'
|
||||
todoBox.id = items[i].itemId
|
||||
todoBox.checked = items[i].record.complete ? true : false
|
||||
todoBox.onclick = (e) => {
|
||||
e.preventDefault()
|
||||
userbase.update('todos', {
|
||||
'todo': items[i].record.todo,
|
||||
'complete': !items[i].record.complete
|
||||
}, items[i].itemId)
|
||||
.catch((e) => document.getElementById('add-todo-error').innerHTML = e)
|
||||
}
|
||||
|
||||
// build the todo label
|
||||
const todoLabel = document.createElement('label')
|
||||
todoLabel.innerHTML = items[i].record.todo
|
||||
|
||||
...
|
||||
|
||||
// append the todo item to the list
|
||||
const todoItem = document.createElement('div')
|
||||
todoItem.appendChild(todoBox)
|
||||
todoItem.appendChild(todoLabel)
|
||||
todosList.appendChild(todoItem)
|
||||
}
|
||||
</code>
|
||||
</pre>
|
||||
|
||||
<h3>Deleting to-dos</h3>
|
||||
|
||||
<p>Let's create a button for deleting a to-do:</p>
|
||||
|
||||
<pre data-line="4-11">
|
||||
<code class="language-javascript">
|
||||
// render all the to-do items
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
|
||||
// build the todo delete button
|
||||
const todoDelete = document.createElement('button')
|
||||
todoDelete.innerHTML = 'X'
|
||||
todoDelete.style.display = 'inline-block'
|
||||
todoDelete.onclick = () => {
|
||||
userbase.delete('todos', items[i].itemId)
|
||||
.catch((e) => document.getElementById('add-todo-error').innerHTML = e)
|
||||
}
|
||||
|
||||
// build the todo checkbox
|
||||
const todoBox = document.createElement('input')
|
||||
todoBox.type = 'checkbox'
|
||||
</code>
|
||||
</pre>
|
||||
|
||||
<p>And append the delete button to to-do element:</p>
|
||||
|
||||
<pre data-line="3">
|
||||
<code class="language-javascript">
|
||||
// append the todo item to the list
|
||||
const todoItem = document.createElement('div')
|
||||
todoItem.appendChild(todoDelete)
|
||||
todoItem.appendChild(todoBox)
|
||||
todoItem.appendChild(todoLabel)
|
||||
todosList.appendChild(todoItem)
|
||||
</code>
|
||||
</pre>
|
||||
|
||||
<h3>Polishing up</h3>
|
||||
|
||||
<p>Before we wrap up, let's add two final pieces of account functionality: user logout and automatic login for users who already have a session.</p>
|
||||
|
||||
<h3>Signing out users</h3>
|
||||
|
||||
<p>First, add a logout button along with a container for displaying error messages:</p>
|
||||
|
||||
<script type="text/plain" data-line="4-5" class="language-markup">
|
||||
<!-- To-dos View -->
|
||||
<div id="todo-view">
|
||||
<div id="username"></div>
|
||||
<input type="button" value="Logout" id="logout-button">
|
||||
<div id="logout-error"></div>
|
||||
|
||||
<h1>To-Do List</h1>
|
||||
<div id="todos"></div>
|
||||
</script>
|
||||
|
||||
<p>Then, add code to handle click events and log out the user:</p>
|
||||
|
||||
<pre data-line="9-13,21-30,40">
|
||||
<code class="language-markup">
|
||||
<!-- application code -->
|
||||
<script type="text/javascript">
|
||||
|
||||
...
|
||||
|
||||
.catch((e) => document.getElementById('create-account-error').innerHTML = e)
|
||||
}
|
||||
|
||||
function handleLogout() {
|
||||
userbase.signOut()
|
||||
.then(() => showAuth())
|
||||
.catch((e) => document.getElementById('logout-error').innerText = e)
|
||||
}
|
||||
|
||||
function showTodos(username) {
|
||||
document.getElementById('auth-view').style.display = 'none'
|
||||
document.getElementById('todo-view').style.display = 'block'
|
||||
|
||||
...
|
||||
|
||||
function showAuth() {
|
||||
document.getElementById('todo-view').style.display = 'none'
|
||||
document.getElementById('auth-view').style.display = 'block'
|
||||
document.getElementById('login-username').value = ''
|
||||
document.getElementById('login-password').value = ''
|
||||
document.getElementById('login-error').innerText = ''
|
||||
document.getElementById('create-account-username').value = ''
|
||||
document.getElementById('create-account-password').value = ''
|
||||
document.getElementById('create-account-error').innerText = ''
|
||||
}
|
||||
|
||||
function handleDatabaseChange(items) {
|
||||
const todosList = document.getElementById('todos')
|
||||
|
||||
...
|
||||
|
||||
document.getElementById('login-form').addEventListener('submit', handleLogin)
|
||||
document.getElementById('signup-form').addEventListener('submit', handleSignUp)
|
||||
document.getElementById('add-todo-form').addEventListener('submit', addTodoHandler)
|
||||
document.getElementById('logout-button').addEventListener('click', handleLogout)
|
||||
document.getElementById('todo-view').style.display = 'none'
|
||||
</script>
|
||||
</code>
|
||||
</pre>
|
||||
|
||||
<p>The logout function calls <code class="language-javascript">userbase.signOut</code> which sends a request to end the user's session to the Userbase service. A Promise is returned that either resolves if the user was signed out successfully, in which case we hide the to-do view and show the account view using showAuth, or rejects with an error, in which case we display the error message.</p>
|
||||
|
||||
<h3>Automatically resuming a session</h3>
|
||||
|
||||
<p>Whenever a new session is created, either by signing up or signing in a user, the Userbase client will store information about the session in browser storage to allow for the session to be resumed when the user returns after having navigating away, whether by closing the page or otherwise.</p>
|
||||
|
||||
<p>Let's modify our app to automatically sign in a user when the page loads. We'll add a view that indicates we are signing in the user:</p>
|
||||
|
||||
<p>Add add a view to show when initializing:</p>
|
||||
|
||||
<pre data-line="4-5">
|
||||
<code class="language-markup">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- Init View -->
|
||||
<div id="init-view">Signing you in...</div>
|
||||
|
||||
<!-- Auth View -->
|
||||
<div id="auth-view">
|
||||
<h1>Login</h1>
|
||||
</code>
|
||||
</pre>
|
||||
|
||||
<p>In order to automatically resume a session if one is available, we add the following to our application code:</p>
|
||||
|
||||
<pre data-line="9,12, 14-17">
|
||||
<code class="language-markup">
|
||||
<!-- application code -->
|
||||
<script type="text/javascript">
|
||||
|
||||
...
|
||||
|
||||
document.getElementById('login-form').addEventListener('submit', handleLogin)
|
||||
document.getElementById('signup-form').addEventListener('submit', handleSignUp)
|
||||
document.getElementById('add-todo-form').addEventListener('submit', addTodoHandler)
|
||||
document.getElementById('logout-button').addEventListener('click', handleLogout)
|
||||
|
||||
document.getElementById('todo-view').style.display = 'none'
|
||||
document.getElementById('auth-view').style.display = 'none'
|
||||
|
||||
userbase.signInWithSession()
|
||||
.then((session) => showTodos(session.username))
|
||||
.catch(() => showAuth())
|
||||
.then(() => document.getElementById('init-view').style.display = 'none')
|
||||
|
||||
</script>
|
||||
</code>
|
||||
</pre>
|
||||
|
||||
<p>
|
||||
We hide the auth view initially, as we'll now only show it if an existing
|
||||
session can't be resumed.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
We make a call to <code class="language-javascript">userbase.signInWithSession</code> to attempt to sign in the user
|
||||
using an existing session as soon as our app loads.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
This function looks for a previous session in browser storage and if one is
|
||||
found tries to sign in the user automatically with the Userbase service. It
|
||||
returns a Promise that will resolve with a new session if the user was able to
|
||||
be signed in or otherwise reject with ann error message? indicating the reason
|
||||
for failure.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
A failure could be due to either no previous session, the user had
|
||||
signed out, or their session expired. In our simple app we'll just send the user
|
||||
to the sign in page regardless of the reason.
|
||||
</p>
|
||||
</div>
|
||||
361
src/prism.css
361
src/prism.css
@@ -1,20 +1,355 @@
|
||||
pre[class*="language-"]:before, pre[class*="language-"]:after {
|
||||
content: normal;
|
||||
/* PrismJS 1.17.1
|
||||
https://prismjs.com/download.html#themes=prism-coy&languages=markup+css+clike+javascript&plugins=line-highlight+toolbar+unescaped-markup+normalize-whitespace+copy-to-clipboard */
|
||||
/**
|
||||
* prism.js Coy theme for JavaScript, CoffeeScript, CSS and HTML
|
||||
* Based on https://github.com/tshedor/workshop-wp-theme (Example: http://workshop.kansan.com/category/sessions/basics or http://workshop.timshedor.com/category/sessions/basics);
|
||||
* @author Tim Shedor
|
||||
*/
|
||||
|
||||
code[class*="language-"],
|
||||
pre[class*="language-"] {
|
||||
color: black;
|
||||
background: none;
|
||||
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
|
||||
font-size: 1em;
|
||||
text-align: left;
|
||||
white-space: pre;
|
||||
word-spacing: normal;
|
||||
word-break: normal;
|
||||
word-wrap: normal;
|
||||
line-height: 1.5;
|
||||
|
||||
-moz-tab-size: 4;
|
||||
-o-tab-size: 4;
|
||||
tab-size: 4;
|
||||
|
||||
-webkit-hyphens: none;
|
||||
-moz-hyphens: none;
|
||||
-ms-hyphens: none;
|
||||
hyphens: none;
|
||||
}
|
||||
|
||||
pre[class*="language-"], :not(pre) > code[class*="language-"], pre[class*="language-"] {
|
||||
margin: 0;
|
||||
line-height: 0.6em;
|
||||
/* Code blocks */
|
||||
pre[class*="language-"] {
|
||||
position: relative;
|
||||
margin: .5em 0;
|
||||
overflow: visible;
|
||||
padding: 0;
|
||||
}
|
||||
pre[class*="language-"]>code {
|
||||
position: relative;
|
||||
border-left: 10px solid #358ccb;
|
||||
box-shadow: -1px 0px 0px 0px #358ccb, 0px 0px 0px 1px #dfdfdf;
|
||||
background-color: #fdfdfd;
|
||||
background-image: linear-gradient(transparent 50%, rgba(69, 142, 209, 0.04) 50%);
|
||||
background-size: 3em 3em;
|
||||
background-origin: content-box;
|
||||
background-attachment: local;
|
||||
}
|
||||
|
||||
code[class*="language"] {
|
||||
padding-top: 1em;
|
||||
padding-bottom: 1em;
|
||||
max-height: inherit;
|
||||
height: inherit;
|
||||
padding: 0 1em;
|
||||
display: block;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* Margin bottom to accommodate shadow */
|
||||
:not(pre) > code[class*="language-"],
|
||||
pre[class*="language-"] {
|
||||
background-color: #fdfdfd;
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
/* Inline code */
|
||||
:not(pre) > code[class*="language-"] {
|
||||
position: relative;
|
||||
padding: .2em;
|
||||
border-radius: 0.3em;
|
||||
color: #c92c2c;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
display: inline;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
pre[class*="language-"]:before,
|
||||
pre[class*="language-"]:after {
|
||||
content: '';
|
||||
z-index: -2;
|
||||
display: block;
|
||||
position: absolute;
|
||||
bottom: 0.75em;
|
||||
left: 0.18em;
|
||||
width: 40%;
|
||||
height: 20%;
|
||||
max-height: 13em;
|
||||
box-shadow: 0px 13px 8px #979797;
|
||||
-webkit-transform: rotate(-2deg);
|
||||
-moz-transform: rotate(-2deg);
|
||||
-ms-transform: rotate(-2deg);
|
||||
-o-transform: rotate(-2deg);
|
||||
transform: rotate(-2deg);
|
||||
}
|
||||
|
||||
:not(pre) > code[class*="language-"]:after,
|
||||
pre[class*="language-"]:after {
|
||||
right: 0.75em;
|
||||
left: auto;
|
||||
-webkit-transform: rotate(2deg);
|
||||
-moz-transform: rotate(2deg);
|
||||
-ms-transform: rotate(2deg);
|
||||
-o-transform: rotate(2deg);
|
||||
transform: rotate(2deg);
|
||||
}
|
||||
|
||||
.token.comment,
|
||||
.token.block-comment,
|
||||
.token.prolog,
|
||||
.token.doctype,
|
||||
.token.cdata {
|
||||
color: #7D8B99;
|
||||
}
|
||||
|
||||
.token.punctuation {
|
||||
color: #5F6364;
|
||||
}
|
||||
|
||||
.token.property,
|
||||
.token.tag,
|
||||
.token.boolean,
|
||||
.token.number,
|
||||
.token.function-name,
|
||||
.token.constant,
|
||||
.token.symbol,
|
||||
.token.deleted {
|
||||
color: #c92c2c;
|
||||
}
|
||||
|
||||
.token.selector,
|
||||
.token.attr-name,
|
||||
.token.string,
|
||||
.token.char,
|
||||
.token.function,
|
||||
.token.builtin,
|
||||
.token.inserted {
|
||||
color: #2f9c0a;
|
||||
}
|
||||
|
||||
.token.operator,
|
||||
.token.entity,
|
||||
.token.url,
|
||||
.token.variable {
|
||||
color: #a67f59;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.token.atrule,
|
||||
.token.attr-value,
|
||||
.token.keyword,
|
||||
.token.class-name {
|
||||
color: #1990b8;
|
||||
}
|
||||
|
||||
.token.regex,
|
||||
.token.important {
|
||||
color: #e90;
|
||||
}
|
||||
|
||||
.language-css .token.string,
|
||||
.style .token.string {
|
||||
color: #a67f59;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.token.important {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.token.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
.token.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.token.entity {
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
.namespace {
|
||||
opacity: .7;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 767px) {
|
||||
pre[class*="language-"]:before,
|
||||
pre[class*="language-"]:after {
|
||||
bottom: 14px;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* Plugin styles */
|
||||
.token.tab:not(:empty):before,
|
||||
.token.cr:before,
|
||||
.token.lf:before {
|
||||
color: #e0d7d1;
|
||||
}
|
||||
|
||||
/* Plugin styles: Line Numbers */
|
||||
pre[class*="language-"].line-numbers.line-numbers {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
pre[class*="language-"].line-numbers.line-numbers code {
|
||||
padding-left: 3.8em;
|
||||
}
|
||||
|
||||
pre[class*="language-"].line-numbers.line-numbers .line-numbers-rows {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
/* Plugin styles: Line Highlight */
|
||||
pre[class*="language-"][data-line] {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
pre[data-line] code {
|
||||
position: relative;
|
||||
padding-left: 4em;
|
||||
}
|
||||
pre .line-highlight {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
pre[data-line] {
|
||||
position: relative;
|
||||
padding: 1em 0 1em 3em;
|
||||
}
|
||||
|
||||
.line-highlight {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: inherit 0;
|
||||
margin-top: 1em; /* Same as .prism’s padding-top */
|
||||
|
||||
background: hsla(24, 20%, 50%,.08);
|
||||
background: linear-gradient(to right, hsla(24, 20%, 50%,.1) 70%, hsla(24, 20%, 50%,0));
|
||||
|
||||
pointer-events: none;
|
||||
|
||||
line-height: inherit;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.line-highlight:before,
|
||||
.line-highlight[data-end]:after {
|
||||
content: attr(data-start);
|
||||
position: absolute;
|
||||
top: .4em;
|
||||
left: .6em;
|
||||
min-width: 1em;
|
||||
padding: 0 .5em;
|
||||
background-color: hsla(24, 20%, 50%,.4);
|
||||
color: hsl(24, 20%, 95%);
|
||||
font: bold 65%/1.5 sans-serif;
|
||||
text-align: center;
|
||||
vertical-align: .3em;
|
||||
border-radius: 999px;
|
||||
text-shadow: none;
|
||||
box-shadow: 0 1px white;
|
||||
}
|
||||
|
||||
.line-highlight[data-end]:after {
|
||||
content: attr(data-end);
|
||||
top: auto;
|
||||
bottom: .4em;
|
||||
}
|
||||
|
||||
.line-numbers .line-highlight:before,
|
||||
.line-numbers .line-highlight:after {
|
||||
content: none;
|
||||
}
|
||||
|
||||
div.code-toolbar {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
div.code-toolbar > .toolbar {
|
||||
position: absolute;
|
||||
top: .3em;
|
||||
right: .2em;
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
div.code-toolbar:hover > .toolbar {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Separate line b/c rules are thrown out if selector is invalid.
|
||||
IE11 and old Edge versions don't support :focus-within. */
|
||||
div.code-toolbar:focus-within > .toolbar {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
div.code-toolbar > .toolbar .toolbar-item {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
div.code-toolbar > .toolbar a {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
div.code-toolbar > .toolbar button {
|
||||
background: none;
|
||||
border: 0;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
line-height: normal;
|
||||
overflow: visible;
|
||||
padding: 0;
|
||||
-webkit-user-select: none; /* for button */
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
|
||||
div.code-toolbar > .toolbar a,
|
||||
div.code-toolbar > .toolbar button,
|
||||
div.code-toolbar > .toolbar span {
|
||||
color: #bbb;
|
||||
font-size: .8em;
|
||||
padding: 0 .5em;
|
||||
background: #f5f2f0;
|
||||
background: rgba(224, 224, 224, 0.2);
|
||||
box-shadow: 0 2px 0 0 rgba(0,0,0,0.2);
|
||||
border-radius: .5em;
|
||||
}
|
||||
|
||||
div.code-toolbar > .toolbar a:hover,
|
||||
div.code-toolbar > .toolbar a:focus,
|
||||
div.code-toolbar > .toolbar button:hover,
|
||||
div.code-toolbar > .toolbar button:focus,
|
||||
div.code-toolbar > .toolbar span:hover,
|
||||
div.code-toolbar > .toolbar span:focus {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Fallback, in case JS does not run, to ensure the code is at least visible */
|
||||
[class*='lang-'] script[type='text/plain'],
|
||||
[class*='language-'] script[type='text/plain'],
|
||||
script[type='text/plain'][class*='lang-'],
|
||||
script[type='text/plain'][class*='language-'] {
|
||||
display: block;
|
||||
font: 100% Consolas, Monaco, monospace;
|
||||
white-space: pre;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
pre[class*="language-"]>code {
|
||||
background-color: #f6f6f6;
|
||||
box-shadow: none;
|
||||
background-image: none;
|
||||
font-size: 0.7em;
|
||||
}
|
||||
12
src/prism.js
Normal file
12
src/prism.js
Normal file
File diff suppressed because one or more lines are too long
@@ -53,7 +53,11 @@ h2 {
|
||||
}
|
||||
|
||||
h3 {
|
||||
@apply font-bold text-xl tracking-tight;
|
||||
@apply font-bold text-xl tracking-tight mt-4;
|
||||
}
|
||||
|
||||
h4 {
|
||||
@apply font-bold text-xl tracking-tight mt-3;
|
||||
}
|
||||
|
||||
@screen sm {
|
||||
@@ -68,6 +72,10 @@ h3 {
|
||||
h3 {
|
||||
@apply text-3xl;
|
||||
}
|
||||
|
||||
h4 {
|
||||
@apply text-2xl;
|
||||
}
|
||||
}
|
||||
|
||||
@screen lg {
|
||||
@@ -115,4 +123,45 @@ a.anchor {
|
||||
top: -50px;
|
||||
}
|
||||
|
||||
pre[class*="language-"]:before, pre[class*="language-"]:after {
|
||||
content: normal;
|
||||
}
|
||||
|
||||
pre[class*="language-"], :not(pre) > code[class*="language-"], pre[class*="language-"] {
|
||||
line-height: 24px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
code[class*="language"] {
|
||||
padding-top: 16px;
|
||||
padding-bottom: 16px;
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
pre[class*="language-"]>code {
|
||||
background-color: #f6f6f6;
|
||||
box-shadow: none;
|
||||
background-image: none;
|
||||
}
|
||||
|
||||
.line-highlight:before, .line-highlight[data-end]:after {
|
||||
content: normal;
|
||||
}
|
||||
|
||||
pre[data-line] code {
|
||||
padding-left: 1em;
|
||||
}
|
||||
|
||||
pre .line-highlight {
|
||||
margin-top: 16px;
|
||||
background-image: none;
|
||||
background-color: rgba(0, 255, 0, 0.12);
|
||||
}
|
||||
|
||||
.token.operator, .token.entity, .token.url, .token.variable {
|
||||
color: #ff8000;
|
||||
background: none;
|
||||
}
|
||||
|
||||
@tailwind utilities;
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header class="sticky top-0 bg-white">
|
||||
<header class="sticky top-0 bg-white z-50">
|
||||
<div class="sm:flex sm:justify-between sm:items-center py-2 px-4">
|
||||
<div class="flex items-center justify-between h-10">
|
||||
<div class="flex-shrink-0">
|
||||
|
||||
Reference in New Issue
Block a user