Merge branch 'master' of github.com:jorilallo/atlas into collection-home

This commit is contained in:
Tom Moor
2017-11-22 18:32:35 -08:00
23 changed files with 726 additions and 348 deletions

View File

@@ -1,47 +1,22 @@
// @flow
import React from 'react';
import { observer, inject } from 'mobx-react';
import { inject } from 'mobx-react';
import { slackAuth } from 'shared/utils/routeHelpers';
import AuthStore from 'stores/AuthStore';
type Props = {
children: React$Element<*>,
scopes?: string[],
auth: AuthStore,
redirectUri: string,
scopes?: string[],
redirectUri?: string,
};
@observer
class SlackAuthLink extends React.Component {
props: Props;
static defaultProps = {
scopes: [
'identity.email',
'identity.basic',
'identity.avatar',
'identity.team',
],
};
slackUrl = () => {
const baseUrl = 'https://slack.com/oauth/authorize';
const params = {
client_id: SLACK_KEY,
scope: this.props.scopes ? this.props.scopes.join(' ') : '',
redirect_uri: this.props.redirectUri || SLACK_REDIRECT_URI,
state: this.props.auth.getOauthState(),
};
const urlParams = Object.keys(params)
.map(key => `${key}=${encodeURIComponent(params[key])}`)
.join('&');
return `${baseUrl}?${urlParams}`;
};
render() {
return <a href={this.slackUrl()}>{this.props.children}</a>;
}
function SlackAuthLink({ auth, children, scopes, redirectUri }: Props) {
return (
<a href={slackAuth(auth.getOauthState(), scopes, redirectUri)}>
{children}
</a>
);
}
export default inject('auth')(SlackAuthLink);

View File

@@ -24,7 +24,6 @@ import Collection from 'scenes/Collection';
import Document from 'scenes/Document';
import Search from 'scenes/Search';
import SlackAuth from 'scenes/SlackAuth';
import Flatpage from 'scenes/Flatpage';
import ErrorAuth from 'scenes/ErrorAuth';
import Error404 from 'scenes/Error404';
@@ -33,8 +32,6 @@ import ScrollToTop from 'components/ScrollToTop';
import Layout from 'components/Layout';
import RouteSidebarHidden from 'components/RouteSidebarHidden';
import flatpages from 'static/flatpages';
import { matchDocumentSlug } from 'utils/routeHelpers';
let DevTools;
@@ -92,7 +89,6 @@ const Auth = ({ children }: AuthProps) => {
};
const notFoundSearch = () => <Search notFound />;
const Api = () => <Flatpage title="API" content={flatpages.api} />;
const DocumentNew = () => <Document newDocument />;
const RedirectDocument = ({ match }: { match: Object }) => (
<Redirect to={`/doc/${match.params.documentSlug}`} />
@@ -141,7 +137,6 @@ render(
<Route exact path="/search" component={Search} />
<Route exact path="/search/:query" component={Search} />
<Route exact path="/developers" component={Api} />
<Route path="/404" component={Error404} />

View File

@@ -1,6 +1,6 @@
// @flow
import React, { Component } from 'react';
import { Link, withRouter } from 'react-router-dom';
import { withRouter } from 'react-router-dom';
import { inject, observer } from 'mobx-react';
import UiStore from 'stores/UiStore';
import AuthStore from 'stores/AuthStore';
@@ -23,6 +23,10 @@ class AccountMenu extends Component {
this.props.ui.setActiveModal('settings');
};
handleApi = () => {
window.location.href = '/developers';
};
handleLogout = () => {
this.props.auth.logout();
window.location.href = BASE_URL;
@@ -40,9 +44,9 @@ class AccountMenu extends Component {
<DropdownMenuItem onClick={this.handleOpenKeyboardShortcuts}>
Keyboard shortcuts
</DropdownMenuItem>
<Link to="/developers">
<DropdownMenuItem>API documentation</DropdownMenuItem>
</Link>
<DropdownMenuItem onClick={this.handleApi}>
API documentation
</DropdownMenuItem>
<DropdownMenuItem onClick={this.handleLogout}>Logout</DropdownMenuItem>
</DropdownMenu>
);

View File

@@ -169,6 +169,7 @@ class DocumentScene extends Component {
if (redirect || this.props.newDocument) {
this.props.history.push(document.url);
this.props.ui.setActiveDocument(document);
}
};

View File

@@ -1,32 +0,0 @@
// @flow
import React from 'react';
import { observer } from 'mobx-react';
import CenteredContent from 'components/CenteredContent';
import Editor from 'components/Editor';
import PageTitle from 'components/PageTitle';
type Props = {
title: string,
content: string,
};
const Flatpage = observer((props: Props) => {
const { title, content } = props;
return (
<CenteredContent>
<PageTitle title={title} />
<Editor
text={content}
onChange={() => {}}
onSave={() => {}}
onCancel={() => {}}
onImageUploadStart={() => {}}
onImageUploadStop={() => {}}
readOnly
/>
</CenteredContent>
);
});
export default Flatpage;

View File

@@ -1,3 +0,0 @@
// @flow
import Flatpage from './Flatpage';
export default Flatpage;

View File

@@ -6,7 +6,7 @@ import queryString from 'query-string';
import { observable } from 'mobx';
import { observer, inject } from 'mobx-react';
import { client } from 'utils/ApiClient';
import { slackAuth } from 'utils/routeHelpers';
import { slackAuth } from 'shared/utils/routeHelpers';
import AuthStore from 'stores/AuthStore';

View File

@@ -1,199 +0,0 @@
# Outline API
_Our API is currently in beta and we might make minor adjustments._
## Making requests
Outline's API follows JSON RPC style conventions where each API endpoint is a method on `https://www.getoutline.com/api/<METHOD>`. Each request needs to be made using HTTPS and both `GET` and `POST` (recommended) methods are supported.
For `GET` requests query string parameters are expected (e.g. `/api/document.info?id=...&token=...`). When making `POST` requests, request parameters are parsed depending on `Content-Type` header. To make a call using JSON payload, one must pass `Content-Type: application/json` header:
```shell
curl 'https://www.getoutline.com/api/documents.info?id=outline-api-NTpezNwhUP'\
-H 'authorization: Bearer <API KEY>'\
-H 'content-type: application/json'\
-H 'accept: application/json'
```
## Authentication
To access private API endpoints, you must provide a valid API key. You can create new API keys in your [account settings](https://www.getoutline.com/settings). Be careful when handling your keys as they give access to all of your documents.
To authenticate with Outline API, you can supply the API key as a header (`Authorization: Bearer <API KEY>`) or as part of the payload using `token` parameter.
Some API endpoints allow unauhenticated requests for public resources and they can be called without an API key.
## Errors
All successful API requests will be returned with `200` status code and `ok: true` in the response payload. If there's an error while making the request, appropriate status code is returned with the `error` message:
```json
{
"ok": false,
"error: "Not Found"
}
```
## Methods
### `user.info` - Get current user
This method returns the information for currently logged in user.
#### Arguments
`https://www.getoutline.com/api/user.info`
Parameter | Description
------------ | -------------
`token` | Authentication token
---
### `user.s3Upload` - Gets S3 upload credentials
You can upload small files and images as part of your documents. All files are stored using Amazon S3. Instead of uploading files to Outline, you need to upload them directly to S3 with special credentials which can be obtained through this endpoint.
#### Arguments
`https://www.getoutline.com/api/user.s3Upload`
Parameter | Description
------------ | -------------
`token` | Authentication token
`filename` | Filename of the uploaded file
`kind` | Mimetype of the document
`size` | Filesize of the document
---
### `collections.list` - List your document collections
List all your document collections.
#### Arguments
`https://www.getoutline.com/api/collections.list`
Parameter | Description
------------ | -------------
`token` | Authentication token
`offset` | Pagination offset
`limit` | Pagination limit
---
### `collections.info` - Get a document collection
Returns detailed information on a document collection.
#### Arguments
`https://www.getoutline.com/api/collections.info`
Parameter | Description
------------ | -------------
`token` | Authentication token
`id` | Collection id
---
### `collections.create` - Create a document collection
Creates a new document collection. Outline supports two types of collections:
- `atlas` - Structured collection with a navigation tree
- `journal` - Chronological collection of documents
#### Arguments
`https://www.getoutline.com/api/collections.create`
Parameter | Description
------------ | -------------
`token` | Authentication token
`name` | Collection name
`type` | Collection type. Allowed values: `atlas`, `journal`
`description` | _(Optional)_ Short description for the collection
---
### `collections.updateNavigationTree` - Organize navigation tree
Collection navigation can be re-organized by sending a modified version of the navigation tree. This method is available for collections with type `atlas`.
#### Arguments
`https://www.getoutline.com/api/collections.updateNavigationTree`
Parameter | Description
------------ | -------------
`token` | Authentication token
`id` | Collection id
`tree` | Modified navigation tree
---
### `documents.info` - Get a document
This method returns information for a document with a specific ID. Following identifiers are allowed:
- UUID - `id` field of the document
- URI identifier - Human readable identifier used in Outline URLs (e.g. `outline-api-i48ZEZc5zjXndcP`)
#### Arguments
`https://www.getoutline.com/api/documents.info`
Parameter | Description
------------ | -------------
`token` | Authentication token
`id` | Document id or URI identifier
---
### `documents.search` - Search documents
This methods allows you to search all of your documents with keywords.
#### Arguments
`https://www.getoutline.com/api/documents.search`
Parameter | Description
------------ | -------------
`token` | Authentication token
`query` | Search query
---
### `documents.create` - Create a new document
This method allows you to publish a new document under an existing collection. If your collection is structured `type: atlas` collection, you can also create sub-documents for other documents with optional `parentDocument` parameter.
#### Arguments
`https://www.getoutline.com/api/documents.create`
Parameter | Description
------------ | -------------
`token` | Authentication token
`collection` | `id` of the collection to which the document is created
`title` | Title for the document
`text` | Content of the document in Markdown
`parentDocument` | _(Optional)_ `id` of the parent document within the collection
---
### `documents.delete` - Delete a document
Delete a document and all of its child documents if any.
#### Arguments
`https://www.getoutline.com/api/documents.delete`
Parameter | Description
------------ | -------------
`token` | Authentication token
`id` | Document id or URI identifier

View File

@@ -1,6 +0,0 @@
// @flow
import api from './api.md';
export default {
api,
};

View File

@@ -53,7 +53,10 @@ class AuthStore {
@action
authWithSlack = async (code: string, state: string) => {
if (state !== this.oauthState) {
// in the case of direct install from the Slack app store the state is
// created on the server and set as a cookie
const serverState = Cookie.get('state', { path: '/' });
if (state !== this.oauthState && state !== serverState) {
return {
success: false,
};
@@ -68,6 +71,9 @@ class AuthStore {
};
}
// State can only ever be used once so now's the time to remove it.
Cookie.remove('state', { path: '/' });
invariant(
res && res.data && res.data.user && res.data.team && res.data.accessToken,
'All values should be available'

View File

@@ -22,31 +22,6 @@ export function documentUrl(doc: Document): string {
return doc.url;
}
export function slackAuth(
state: string,
scopes: string[] = [
'identity.email',
'identity.basic',
'identity.avatar',
'identity.team',
],
redirectUri: string = `${BASE_URL}/auth/slack`
): string {
const baseUrl = 'https://slack.com/oauth/authorize';
const params = {
client_id: SLACK_KEY,
scope: scopes ? scopes.join(' ') : '',
redirect_uri: redirectUri,
state,
};
const urlParams = Object.keys(params)
.map(key => `${key}=${encodeURIComponent(params[key])}`)
.join('&');
return `${baseUrl}?${urlParams}`;
}
export function documentNewUrl(doc: Document): string {
const newUrl = `${doc.collection.url}/new`;
if (doc.parentDocumentId) {

View File

@@ -10,7 +10,7 @@
"NODE_ENV=production webpack --config webpack.config.prod.js --json | webpack-bundle-size-analyzer",
"build": "npm run clean && npm run build:webpack",
"start": "NODE_ENV=production node index.js",
"dev": "NODE_ENV=development nodemon --inspect --watch server index.js",
"dev": "NODE_ENV=development nodemon --watch server index.js",
"lint": "npm run lint:flow && npm run lint:js",
"lint:js": "eslint app",
"lint:flow": "flow",
@@ -152,7 +152,7 @@
"slate-collapse-on-escape": "^0.2.1",
"slate-edit-code": "^0.10.2",
"slate-edit-list": "^0.7.0",
"slate-md-serializer": "0.5.5",
"slate-md-serializer": "0.5.6",
"slate-paste-linkify": "^0.2.1",
"slate-prism": "^0.2.2",
"slate-trailing-block": "^0.2.4",

View File

@@ -26,24 +26,30 @@ router.post('hooks.slack', async ctx => {
limit: 5,
});
const results = [];
let number = 1;
for (const document of documents) {
results.push(
`${number}. <${process.env.URL}${document.getUrl()}|${document.title}>`
);
number += 1;
}
if (documents) {
const results = [];
let number = 1;
for (const document of documents) {
results.push(
`${number}. <${process.env.URL}${document.getUrl()}|${document.title}>`
);
number += 1;
}
ctx.body = {
text: 'Search results:',
attachments: [
{
text: results.join('\n'),
color: '#3AA3E3',
},
],
};
ctx.body = {
text: 'Search results:',
attachments: [
{
text: results.join('\n'),
color: '#3AA3E3',
},
],
};
} else {
ctx.body = {
text: 'No search results',
};
}
});
export default router;

559
server/pages/Api.js Normal file
View File

@@ -0,0 +1,559 @@
// @flow
import React from 'react';
import Grid from 'styled-components-grid';
import { Helmet } from 'react-helmet';
import styled from 'styled-components';
const Container = styled.div`
max-width: 720px;
margin: 0 auto;
pre {
padding: 0.5em 1em;
background: #f9fbfc;
border-radius: 4px;
border: 1px solid #e8ebed;
overflow: scroll;
}
code {
font-size: 15px;
}
table {
border-collapse: collapse;
thead {
td {
padding: 5px 12px 5px 0;
border-bottom: 1px solid #ddd;
vertical-align: bottom;
font-weight: 500;
}
}
tbody,
thead {
td {
padding: 5px 12px 5px 0;
}
td:last-child {
width: 100%;
padding-right: 0;
}
}
}
h3 {
code {
font-size: 1.08em;
}
}
`;
export default function Pricing() {
return (
<Grid>
<Helmet>
<title>Developer API - Outline</title>
</Helmet>
<Container>
<h1>Outline API</h1>
<p>
First thing we build for Outline was its API. Its the heart and soul
of the service and as developers, its our mission to make the API as
rich and easy to use as possible.
</p>
<p>
<i>
While Outline is still in public beta, we might make small
adjustments, including breaking changes to the API.
</i>
</p>
<h2>Making requests</h2>
<p>
Outlines API follows simple RPC style conventions where each API
endpoint is a method on{' '}
<code>https://www.getoutline.com/api/&lt;METHOD&gt;</code>. Both{' '}
<code>GET</code> and <code>POST</code> methods are supported but its
recommeded that you make all call using <code>POST</code>. Only HTTPS
is supported in production.
</p>
<p>
For <code>GET</code> requests query string parameters are expected
(e.g.
<code>/api/document.info?id=...&token=...</code>). When making{' '}
<code>POST</code> requests, request parameters are parsed depending on{' '}
<code>Content-Type</code> header. To make a call using JSON payload,
one must pass <code>Content-Type: application/json</code> header:
</p>
<p>
<strong>Example POST request:</strong>
</p>
<pre>
<code>
{`curl https://www.getoutline.com/api/documents.info
-X POST
-H 'authorization: Bearer API_KEY'
-H 'content-type: application/json'
-H 'accept: application/json'
-d '{"id": "outline-api-NTpezNwhUP"}'
`}
</code>
</pre>
<p>
<strong>Example GET request:</strong>
</p>
<pre>
<code>
{`curl https://www.getoutline.com/api/documents.info?id=outline-api-NTpezNwhUP&token=API_KEY
`}
</code>
</pre>
<h2>Authentication</h2>
<p>
To access private API endpoints, you must provide a valid API key. You
can create new API keys in your{' '}
<a href={`${process.env.URL}/settings`}>account settings</a>. Be
careful when handling your keys as they give access to all of your
documents.
</p>
<p>
To authenticate with Outline API, you can supply the API key as a
header (<code>Authorization: Bearer YOUR_API_KEY</code>) or as part of
the payload using <code>token</code> parameter. If you're making{' '}
<code>GET</code> requests, header based authentication is recommended
so that your keys don't leak into logs.
</p>
<p>
Some API endpoints allow unauhenticated requests for public resources
and they can be called without an API key.
</p>
<h2>Errors</h2>
<p>
All successful API requests will be returned with <code>200</code>{' '}
status code and <code>ok: true</code> in the response payload. If
theres an error while making the request, appropriate status code is
returned with the <code>error</code> message:
</p>
<pre>
<code>
{`{
"ok": false,
"error: "Not Found"
}
`}
</code>
</pre>
<h2>Methods</h2>
<Methods>
<Method method="user.info" label="Get current user">
<Description>
This method returns the information for currently logged in user.
</Description>
<Arguments>
<Argument id="id" description="Collection id" required />
</Arguments>
</Method>
<Method method="user.s3Upload" label="Get S3 upload credentials">
<Description>
You can upload small files and images as part of your documents.
All files are stored using Amazon S3. Instead of uploading files
to Outline, you need to upload them directly to S3 with special
credentials which can be obtained through this endpoint.
</Description>
<Arguments>
<Argument
id="filename"
description="Filename of the uploaded file"
required
/>
<Argument
id="kind"
description="Mimetype of the document"
required
/>
<Argument
id="size"
description="Filesize of the document"
required
/>
</Arguments>
</Method>
<Method
method="collections.list"
label="List your document collections"
>
<Description>List all your document collections.</Description>
<Arguments pagination />
</Method>
<Method method="collections.info" label="Get a document collection">
<Description>
Returns detailed information on a document collection.
</Description>
<Arguments>
<Argument id="id" description="Collection id" required />
</Arguments>
</Method>
<Method
method="collections.create"
label="Create a document collection"
>
<Description>Creates a new document collection.</Description>
<Arguments>
<Argument id="name" description="Collection name" required />
<Argument
id="description"
description="Short description for the collection"
/>
</Arguments>
</Method>
<Method method="collections.update" label="Update a collection">
<Description>
This method allows you to modify already created document.
</Description>
<Arguments>
<Argument id="id" description="Collection ID" required />
<Argument id="name" description="Name for the collection" />
<Argument
id="color"
description="Collection color in hex form (e.g. #E1E1E1)"
/>
</Arguments>
</Method>
<Method method="collections.delete" label="Delete a collection">
<Description>
Delete a collection and all of its documents. This action can`t be
undone so please be careful.
</Description>
<Arguments>
<Argument id="id" description="Collection ID" required />
</Arguments>
</Method>
<Method method="documents.info" label="Get a document">
<Description>
<p>
This method returns information for a document with a specific
ID. Following identifiers are allowed:
</p>
<ul>
<li>
UUID - <code>id</code> field of the document
</li>
<li>
URI identifier - Human readable identifier used in Outline
URLs (e.g. <code>outline-api-i48ZEZc5zjXndcP</code>)
</li>
</ul>
</Description>
<Arguments>
<Argument
id="id"
description="Document id or URI identifier"
required
/>
</Arguments>
</Method>
<Method method="documents.search" label="Search documents">
<Description>
This methods allows you to search all of your documents with
keywords.
</Description>
<Arguments>
<Argument id="query" description="Search query" required />
</Arguments>
</Method>
<Method method="documents.create" label="Create a new document">
<Description>
This method allows you to publish a new document under an existing
collection. By default a document is set to the parent collection
root. If you want to create a subdocument, you can pass{' '}
<code>parentDocument</code> to set parent document.
</Description>
<Arguments>
<Argument
id="collection"
description={
<span>
<code>ID</code> of the collection to which the document is
created
</span>
}
required
/>
<Argument
id="title"
description="Title for the document"
required
/>
<Argument
id="text"
description="Content of the document in Markdow"
required
/>
<Argument
id="parentDocument"
description={
<span>
<code>ID</code> of the parent document within the collection
</span>
}
/>
</Arguments>
</Method>
<Method method="documents.update" label="Update a document">
<Description>
This method allows you to modify already created document.
</Description>
<Arguments>
<Argument
id="id"
description="Document id or URI identifier"
required
/>
<Argument id="title" description="Title for the document" />
<Argument
id="text"
description="Content of the document in Markdown"
/>
</Arguments>
</Method>
<Method method="documents.move" label="Move document in a collection">
<Description>
Move a document into a new location inside the collection. This is
easily done by defining the parent document ID and optional index.
If no parent document is provided, the document will be moved to
the collection root.
</Description>
<Arguments>
<Argument
id="id"
description="Document id or URI identifier"
required
/>
<Argument
id="parentDocument"
description="ID of the new parent document (if any)"
/>
<Argument id="index" description="Index of the new location" />
</Arguments>
</Method>
<Method method="documents.delete" label="Delete a document">
<Description>
Delete a document and all of its child documents if any.
</Description>
<Arguments>
<Argument
id="id"
description="Document id or URI identifier"
required
/>
</Arguments>
</Method>
<Method method="documents.info" label="Get a document">
<Description>
Get a document with its ID or URL identifier from users
collections.
</Description>
<Arguments>
<Argument
id="id"
description="Document id or URI identifier"
required
/>
</Arguments>
</Method>
<Method method="documents.star" label="Star a document">
<Description>
Star (favorite) a document for authenticated user.
</Description>
<Arguments>
<Argument
id="id"
description="Document id or URI identifier"
required
/>
</Arguments>
</Method>
<Method method="documents.unstar" label="Unstar a document">
<Description>
Unstar as starred (favorited) a document for authenticated user.
</Description>
<Arguments>
<Argument
id="id"
description="Document id or URI identifier"
required
/>
</Arguments>
</Method>
<Method
method="documents.viewed"
label="Get recently viewed document for user"
>
<Description>
Return recently viewed documents for the authenticated user
</Description>
<Arguments pagination />
</Method>
<Method
method="documents.starred"
label="Get recently starred document for user"
>
<Description>
Return recently starred documents for the authenticated user
</Description>
<Arguments pagination />
</Method>
<Method
method="documents.revisions"
label="Get revisions for a document"
>
<Description>
Return revisions for a document. Upon each edit, a new revision is
stored.
</Description>
<Arguments pagination />
</Method>
</Methods>
</Container>
</Grid>
);
}
const MethodList = styled.ul`
margin-bottom: 80px;
`;
const Methods = (props: { children: React.Element<*> }) => {
const children = React.Children.toArray(props.children);
const methods = children.map(child => child.props.method);
return (
<div>
<MethodList>
{methods.map(method => (
<li key={method}>
<a href={`#${method}`}>{method}</a>
</li>
))}
</MethodList>
{children}
</div>
);
};
const MethodContainer = styled.div`
margin-bottom: 80px;
`;
const Request = styled.h4`
text-transform: capitalize;
`;
type MethodProps = {
method: string,
label: string,
children: React.Element<*>,
};
const Method = (props: MethodProps) => {
const children = React.Children.toArray(props.children);
const description = children.find(child => child.type === Description);
const apiArgs = children.find(child => child.type === Arguments);
return (
<MethodContainer>
<h3 id={props.method}>
<code>{props.method}</code> - {props.label}
</h3>
<div>{description}</div>
<Request>HTTP request & arguments</Request>
<p>
<code>{`${process.env.URL}/api/${props.method}`}</code>
</p>
{apiArgs}
</MethodContainer>
);
};
const Description = (props: { children: React.Element<*> }) => (
<p>{props.children}</p>
);
type ArgumentsProps = {
pagination?: boolean,
children?: React.Element<*> | string,
};
const Arguments = (props: ArgumentsProps) => (
<table>
<thead>
<tr>
<td>Argument</td>
<td>Required</td>
<td>Description</td>
</tr>
</thead>
<tbody>
<Argument id="token" description="Authentication token" required />
{props.pagination && (
// $FlowIssue
<PaginationArguments />
)}
{props.children}
</tbody>
</table>
);
type ArgumentProps = {
id: string,
required?: boolean,
description: React.Element<*> | string,
};
const Argument = (props: ArgumentProps) => (
<tr>
<td>
<code>{props.id}</code>
</td>
<td>
<i>{props.required ? 'required' : 'optional'}</i>
</td>
<td>{props.description}</td>
</tr>
);
const PaginationArguments = () => [
<Argument id="offset" description="Pagination offset" />,
<Argument id="limit" description="Pagination limit" />,
];

View File

@@ -14,8 +14,8 @@ function Home() {
<Hero>
<h1>Your teams knowledge base</h1>
<HeroText>
Documentation, meeting notes, playbooks, onboarding, work logs,
brainstorming, decisions, & more
Team wiki, documentation, meeting notes, playbooks, onboarding, work
logs, brainstorming, & more
</HeroText>
<p>
<SignupButton />
@@ -24,7 +24,7 @@ function Home() {
<Features>
<Grid.Unit size={{ desktop: 1 / 3, tablet: 1 / 2 }}>
<Feature>
<h2>Blazing Fast</h2>
<h2>Blazing Fast Wiki</h2>
<p>
Outline is fast, really fast. Weve worked hard to ensure
millisecond response times, documents load instantly, search is
@@ -34,9 +34,10 @@ function Home() {
<Feature>
<h2># Markdown Support</h2>
<p>
Outline stores all documents in plain Markdown. Shortcuts are
also built right into the editor so you can easily format using{' '}
<strong>**markdown syntax**</strong> if you like.
Outline stores, imports and exports all documents in plain
Markdown. Shortcuts are also built right into the editor so you
can easily format using <strong>**markdown syntax**</strong> if
you like.
</p>
</Feature>
</Grid.Unit>
@@ -85,9 +86,9 @@ function Home() {
<p>
On the same page as us? Create a beta account to give Outline a try.
</p>
<p>
<FooterCTA>
<SignupButton />
</p>
</FooterCTA>
</Footer>
</Grid>
</span>
@@ -134,6 +135,10 @@ const Footer = styled.div`
padding: 6em;
`;
const FooterCTA = styled.p`
padding-top: 1em;
`;
const HeroText = styled.p`
font-size: 18px;
max-width: 600px;

View File

@@ -4,6 +4,7 @@ import styled from 'styled-components';
const Hero = styled.div`
width: 100%;
height: 70vh;
max-height: 600px;
padding: 6em 2em 0;
text-align: center;

View File

@@ -1,7 +1,7 @@
// @flow
import React from 'react';
import { Helmet } from 'react-helmet';
import Navigation from './Navigation';
import { TopNavigation, BottomNavigation } from './Navigation';
import Analytics from '../../../shared/components/Analytics';
import globalStyles from '../../../shared/styles/globals';
import { color } from '../../../shared/styles/constants';
@@ -48,8 +48,9 @@ export default function Layout({ children }: Props) {
{'{{CSS}}'}
</head>
<body>
<Navigation />
<TopNavigation />
{children}
<BottomNavigation />
</body>
</html>
);

View File

@@ -1,10 +1,17 @@
// @flow
import React from 'react';
import styled from 'styled-components';
import { signin, developers, blogUrl } from '../../utils/routeHelpers';
import {
signin,
developers,
githubUrl,
spectrumUrl,
blogUrl,
twitterUrl,
} from '../../utils/routeHelpers';
import { color } from '../../../shared/styles/constants';
function Navigation() {
function TopNavigation() {
return (
<Nav>
<Brand href="/">Outline</Brand>
@@ -26,12 +33,38 @@ function Navigation() {
);
}
function BottomNavigation() {
return (
<BottomNav>
<Menu>
<MenuItem>
<a href={githubUrl()}>GitHub</a>
</MenuItem>
<MenuItem>
<a href={spectrumUrl()}>Spectrum</a>
</MenuItem>
<MenuItem>
<a href={blogUrl()}>Medium</a>
</MenuItem>
<MenuItem>
<a href={twitterUrl()}>Twitter</a>
</MenuItem>
</Menu>
</BottomNav>
);
}
const Nav = styled.nav`
display: flex;
padding: 20px 30px;
justify-content: space-between;
`;
const BottomNav = styled(Nav)`
margin-bottom: 30px;
justify-content: center;
`;
const Menu = styled.ul`
margin: 0;
padding: 0;
@@ -52,6 +85,10 @@ const MenuItem = styled.li`
color: ${color.slateDark};
text-decoration: underline;
}
&:first-child {
margin-left: 0;
}
`;
const Brand = styled.a`
@@ -61,4 +98,4 @@ const Brand = styled.a`
color: ${color.black};
`;
export default Navigation;
export { TopNavigation, BottomNavigation };

View File

@@ -8,10 +8,12 @@ import sendfile from 'koa-sendfile';
import serve from 'koa-static';
import subdomainRedirect from './middlewares/subdomainRedirect';
import renderpage from './utils/renderpage';
import { slackAuth } from '../shared/utils/routeHelpers';
import Home from './pages/Home';
import About from './pages/About';
import Pricing from './pages/Pricing';
import Api from './pages/Api';
const isProduction = process.env.NODE_ENV === 'production';
const koa = new Koa();
@@ -43,9 +45,23 @@ if (process.env.NODE_ENV === 'production') {
});
}
// slack direct install
router.get('/auth/slack/install', async ctx => {
const state = Math.random()
.toString(36)
.substring(7);
ctx.cookies.set('state', state, {
httpOnly: false,
expires: new Date('2100'),
});
ctx.redirect(slackAuth(state));
});
// static pages
router.get('/about', ctx => renderpage(ctx, <About />));
router.get('/pricing', ctx => renderpage(ctx, <Pricing />));
router.get('/developers', ctx => renderpage(ctx, <Api />));
// home page
router.get('/', async ctx => {

View File

@@ -8,6 +8,14 @@ export function blogUrl(): string {
return 'https://medium.com/getoutline';
}
export function twitterUrl(): string {
return 'https://twitter.com/getoutline';
}
export function spectrumUrl(): string {
return 'https://spectrum.chat/outline';
}
export function developers(): string {
return '/developers';
}

View File

@@ -0,0 +1,26 @@
// @flow
export function slackAuth(
state: string,
scopes: string[] = [
'identity.email',
'identity.basic',
'identity.avatar',
'identity.team',
],
redirectUri: string = `${process.env.URL}/auth/slack`
): string {
const baseUrl = 'https://slack.com/oauth/authorize';
const params = {
client_id: process.env.SLACK_KEY,
scope: scopes ? scopes.join(' ') : '',
redirect_uri: redirectUri,
state,
};
const urlParams = Object.keys(params)
.map(key => `${key}=${encodeURIComponent(params[key])}`)
.join('&');
return `${baseUrl}?${urlParams}`;
}

View File

@@ -11,10 +11,13 @@ const definePlugin = new webpack.DefinePlugin({
JSON.parse(process.env.BUILD_PRERELEASE || 'false')
),
SLACK_REDIRECT_URI: JSON.stringify(process.env.SLACK_REDIRECT_URI),
SLACK_KEY: JSON.stringify(process.env.SLACK_KEY),
BASE_URL: JSON.stringify(process.env.URL),
BUGSNAG_KEY: JSON.stringify(process.env.BUGSNAG_KEY),
DEPLOYMENT: JSON.stringify(process.env.DEPLOYMENT || 'hosted')
DEPLOYMENT: JSON.stringify(process.env.DEPLOYMENT || 'hosted'),
'process.env': {
URL: JSON.stringify(process.env.URL),
SLACK_KEY: JSON.stringify(process.env.SLACK_KEY),
}
});
module.exports = {

View File

@@ -8094,9 +8094,9 @@ slate-edit-list@^0.7.0:
version "0.7.1"
resolved "https://registry.yarnpkg.com/slate-edit-list/-/slate-edit-list-0.7.1.tgz#84ee960d2d5b5a20ce267ad9df894395a91b93d5"
slate-md-serializer@0.5.5:
version "0.5.5"
resolved "https://registry.npmjs.org/slate-md-serializer/-/slate-md-serializer-0.5.5.tgz#38b73b4867fff2d46b955df11726a0819838d359"
slate-md-serializer@0.5.6:
version "0.5.6"
resolved "https://registry.npmjs.org/slate-md-serializer/-/slate-md-serializer-0.5.6.tgz#88048ae62757ce3aaf1096ebd4200c58fdd1e86c"
slate-paste-linkify@^0.2.1:
version "0.2.1"