web/front.tsx
import React from 'react'
import Router from 'next/router'
import { mutate } from 'swr'
import ourbigbook from 'ourbigbook';
import { webApi } from 'front/api'
import { docsUrl } from 'front/config'
import { AUTH_COOKIE_NAME } from 'front/js'
import CustomLink from 'front/CustomLink'
import routes from 'front/routes'
import { ArticleType } from 'front/types/ArticleType'
import { IssueType } from 'front/types/IssueType'
import { UserLinkWithImageInner } from 'front/UserLinkWithImage'
export const AUTH_LOCAL_STORAGE_NAME = 'user'
export const LOGIN_ACTION = 'Sign in'
export const REGISTER_ACTION = 'Sign up'
export function capitalize(s) {
return s[0].toUpperCase() + s.slice(1)
}
export function decapitalize(s) {
return s[0].toLowerCase() + s.slice(1)
}
export function ArticleBy(
{
article,
newTab=false
}: {
article?: ArticleType,
newTab?: boolean,
}
) {
const inner = <>
"<span
className="comment-body ourbigbook-title"
dangerouslySetInnerHTML={{ __html: article.titleRender }}
/>" by <UserLinkWithImageInner {...{
user: article.author,
showUsername: false,
}} />
</>
const href = routes.article(article.slug)
if (newTab) {
return <a href={href} target="_blank">{inner}</a>
} else {
return <CustomLink href={href}>{inner}</CustomLink>
}
}
export function IssueBy(
{ article }:
{ article?: ArticleType }
) {
return <CustomLink href={routes.article(article.slug)}>
"<span
className="comment-body ourbigbook-title"
dangerouslySetInnerHTML={{ __html: article.titleRender }}
/>" by { article.author.displayName }
</CustomLink>
}
export function DiscussionAbout(
{ article, children, issue, }:
{ article?: ArticleType; issue?: IssueType, children?: React.ReactNode }
) {
const inner = <>
<IssueIcon />{' '}
Discussion{issue ? ` #${issue.number}` : ''} on {' '}
<ArticleBy {...{article, issue}} />
{children}
</>
if (issue) {
return <span className="h2">{ inner }</span>
} else {
return <h1 className="h2">{ inner }</h1>
}
}
// Icons.
export function Icon(cls, title, opts) {
const showTitle = opts.title === undefined ? true : opts.title
const extraClasses = opts.extraClasses === undefined ? [] : opts.extraClasses
return <i className={extraClasses.concat([cls, 'icon']).join(' ')} title={showTitle ? title : undefined } />
}
export function ArticleIcon(opts) {
return Icon("ion-ios-book", "Article", opts)
}
export function ArrowUpIcon(opts) {
return Icon("ion-arrow-up-c", undefined, opts)
}
export function CancelIcon(opts) {
return Icon("ion-close", "Cancel", opts)
}
export function DeleteIcon(opts) {
return Icon("ion-ios-trash", "Delete", opts)
}
export function EditArticleIcon(opts) {
return Icon("ion-edit", "Edit", opts)
}
export function ErrorIcon(opts) {
return Icon("ion-close", "Edit", opts)
}
export function HelpIcon(opts={}) {
return Icon("ion-help-circled", "Help", opts)
}
export function HomeIcon(opts) {
return Icon("ion-android-home", "Home", opts)
}
export function IssueIcon(opts) {
return Icon("ion-ios-chatbubble", "Discussion", opts)
}
export function LikeIcon(opts) {
return Icon("ion-heart", "Like", opts)
}
export function NewArticleIcon(opts) {
return Icon("ion-plus", "New", opts)
}
export function NotificationIcon(opts) {
return Icon("i ion-ios-bell", "Notifications", opts)
}
export function PinnedArticleIcon(opts) {
return Icon("ion-pin", "Pinned Article", opts)
}
export function SeeIcon(opts) {
return Icon("ion-eye", "View", opts)
}
export function SettingsIcon(opts) {
return Icon("ion-gear-a", "Settings", opts)
}
export function SourceIcon(opts) {
return Icon("ion-document-text", "View", opts)
}
export function TimeIcon(opts) {
return Icon("ion-android-time", undefined, opts)
}
export function TopicIcon(opts) {
return Icon("ion-ios-people", "Topic", opts)
}
export function UserIcon(opts) {
return Icon("ion-ios-person", "User", opts)
}
export function SignupOrLogin(
{ to }:
{ to: string }
) {
return <>
<CustomLink href={routes.userNew()}>
{REGISTER_ACTION}
</CustomLink>
{' '}or{' '}
<CustomLink href={routes.userLogin()}>
{decapitalize(LOGIN_ACTION)}
</CustomLink>
{' '}{to}.
</>
}
export function TopicsHelp() {
return <div><HelpIcon /> New to topics? <a href={`${docsUrl}/ourbigbook-web-topics`}>Read the documentation here!</a></div>
}
export function disableButton(btn, msg='Cannot submit due to errors') {
btn.setAttribute('disabled', '')
btn.setAttribute('title', msg)
}
export function enableButton(btn) {
btn.removeAttribute('disabled')
btn.removeAttribute('title')
}
/// Logout the current user on web UI.
export function logout() {
window.localStorage.removeItem(AUTH_LOCAL_STORAGE_NAME);
deleteCookie(AUTH_COOKIE_NAME)
mutate('user', null);
}
export function slugFromArray(arr, { username }: { username?: boolean } = {}) {
if (username === undefined) {
username = true
}
const start = username ? 0 : 1
return arr.slice(start).join(ourbigbook.Macro.HEADER_SCOPE_SEPARATOR)
}
export function slugFromRouter(router, opts={}) {
let arr = router.query.slug
if (!arr) {
return router.query.uid
}
return slugFromArray(arr, opts)
}
export const AppContext = React.createContext<{
title: string
setTitle: React.Dispatch<any> | undefined
prevPageNoSignup: string
updatePrevPageNoSignup: (newCur: string) => void | undefined,
}>({
title: '',
setTitle: undefined,
prevPageNoSignup: '',
updatePrevPageNoSignup: undefined
});
// Global state.
export const AppContextProvider = ({ children, vals }) => {
const [title, setTitle] = React.useState('')
return <AppContext.Provider value={
Object.assign({
title, setTitle,
}, vals)
}>
{children}
</AppContext.Provider>
};
export function useCtrlEnterSubmit(handleSubmit) {
React.useEffect(() => {
function ctrlEnterListener(e) {
if (e.code === 'Enter' && e.ctrlKey) {
handleSubmit(e)
}
}
document.addEventListener('keydown', ctrlEnterListener);
return () => {
document.removeEventListener('keydown', ctrlEnterListener);
};
}, [handleSubmit]);
}
export function useEEdit(canEdit, slug) {
React.useEffect(() => {
function listener(e) {
if (e.code === 'KeyE') {
if (canEdit) {
Router.push(routes.articleEdit(slug))
}
}
}
if (slug !== undefined) {
document.addEventListener('keydown', listener);
return () => {
document.removeEventListener('keydown', listener);
}
}
}, [canEdit, slug]);
}
// https://stackoverflow.com/questions/4825683/how-do-i-create-and-read-a-value-from-cookie/38699214#38699214
export function setCookie(name, value, days?: number, path = '/') {
let delta
if (days === undefined) {
delta = Number.MAX_SAFE_INTEGER
} else {
delta = days * 864e5
}
const expires = new Date(Date.now() + delta).toUTCString()
document.cookie = `${name}=${encodeURIComponent(
value
)};expires=${expires};path=${path}`
}
export function setCookies(cookieDict, days, path = '/') {
for (let key in cookieDict) {
setCookie(key, cookieDict[key], days, path)
}
}
export function getCookie(name) {
return getCookieFromString(document.cookie, name)
}
export function getCookieFromReq(req, name) {
const cookie = req.headers.cookie
if (cookie) {
return getCookieFromString(cookie, name)
} else {
return null
}
}
export function getCookieFromString(s, name) {
return getCookiesFromString(s)[name]
}
// https://stackoverflow.com/questions/5047346/converting-strings-like-document-cookie-to-objects
export function getCookiesFromString(s) {
return s.split('; ').reduce((prev, current) => {
const [name, ...value] = current.split('=')
prev[name] = value.join('=')
return prev
}, {})
}
export function deleteCookie(name, path = '/') {
setCookie(name, '', -1, path)
}
export async function setupUserLocalStorage(user, setErrors?) {
// We fetch from /profiles/:username again because the return from /users/login above
// does not contain the image placeholder.
const { data: userData, status: userStatus } = await webApi.user(
user.username
)
user.effectiveImage = userData.effectiveImage
window.localStorage.setItem(
AUTH_LOCAL_STORAGE_NAME,
JSON.stringify(user)
);
setCookie(AUTH_COOKIE_NAME, user.token)
mutate(AUTH_COOKIE_NAME, user.token)
mutate(AUTH_LOCAL_STORAGE_NAME, user);
}