Skip to content
This repository has been archived by the owner on Mar 19, 2021. It is now read-only.

Commit

Permalink
HOW-40 add gravatar to settings (#99)
Browse files Browse the repository at this point in the history
HOW-40 add gravatar to settings
  • Loading branch information
sergeysova committed Sep 28, 2019
2 parents f462c93 + f7c6829 commit 139d8ff
Show file tree
Hide file tree
Showing 15 changed files with 319 additions and 130 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@
"eslint-plugin-react": "^7.12.4",
"eslint-plugin-unicorn": "^7.1.0",
"file-loader": "2.0.0",
"flow-bin": "^0.96.0",
"flow-bin": "^0.108.0",
"fork-ts-checker-webpack-plugin-alt": "0.4.14",
"formik": "^1.5.1",
"fs-extra": "7.0.1",
Expand All @@ -116,6 +116,7 @@
"jest-styled-components": "^6.3.1",
"jest-watch-typeahead": "^0.2.1",
"lint-staged": "^8.1.4",
"md5": "^2.2.1",
"mini-css-extract-plugin": "0.5.0",
"nanoid": "^2.0.1",
"optimize-css-assets-webpack-plugin": "5.0.1",
Expand Down
2 changes: 2 additions & 0 deletions src/api/account.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,12 @@ const createAccount = (registerData: RegisterData): Promise<number> =>
export type Settings = {|
displayName: string | null,
gravatarEmail: string | null,
currentEmail: string | null,
|}

export type UpdateSettings = {|
displayName?: string,
gravatarEmail?: string,
|}

const updateSettings = (
Expand Down
4 changes: 2 additions & 2 deletions src/features/users/api.js → src/api/users.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
// @flow
import { request } from "@features/common"
import { type Card } from "@api/cards"
import { type User } from "./types"
import { type User } from "@api/account"

/**
* Get info about user
*/
const getInfo = (userId: number): Promise<User> =>
const getInfo = (userId: number): Promise<{ user: User }> =>
request("GET", `/users/${userId}/`)

/**
Expand Down
4 changes: 3 additions & 1 deletion src/features/users/index.js
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export { usersRoutes } from "./routes"
export { ErrorView } from "./organisms/error"
export { LoadingView } from "./organisms/loading"
export { UsersCommonTemplate } from "./templates/common"
9 changes: 0 additions & 9 deletions src/features/users/routes.js

This file was deleted.

Empty file removed src/features/users/types.js
Empty file.
6 changes: 6 additions & 0 deletions src/pages/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { CardViewPage } from "./view/page"
import { JoinLoginPage } from "./join/login/page"
import { JoinRegistrationPage } from "./join/registration/page"
import { SettingsPage } from "./settings/page"
import { UserPage } from "./users/current/page"

export const routes = () => [
{
Expand Down Expand Up @@ -43,4 +44,9 @@ export const routes = () => [
exact: true,
component: SettingsPage,
},
{
path: "/user/:userId",
exact: true,
component: UserPage,
},
]
155 changes: 155 additions & 0 deletions src/pages/settings/model.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
// @flow
import {
type Store,
combine,
createEffect,
createEvent,
createStore,
forward,
sample,
} from "effector"
import md5 from "md5"
import { type Settings, accountApi } from "@api/account"
import { loadSession } from "@features/common"

/**
* 1. load user settings object
* 2. mark page ready
*/

type InputEvent = SyntheticEvent<HTMLInputElement>
type ButtonEvent = SyntheticEvent<HTMLButtonElement>
type FormEvent = SyntheticEvent<HTMLFormElement>

export const pageMounted = createEvent<void>()
export const pageUnmounted = createEvent<void>()

export const nameChanged = createEvent<InputEvent>()
export const nameSubmitted = createEvent<FormEvent>()

export const avaChangePressed = createEvent<ButtonEvent>()
export const gravatarEmailChanged = createEvent<InputEvent>()
export const gravatarEmailSubmitted = createEvent<FormEvent>()
export const gravatarChangeCancelled = createEvent<ButtonEvent>()

const saveGravatar = createEvent<string>()
const saveName = createEvent<string>()

const loadSettings = createEffect()
const saveSettings = createEffect()

export const $settings: Store<?Settings> = createStore(null)
export const $isSettingsReady = $settings.map<boolean>(Boolean)
export const $isLoading: Store<boolean> = combine(
loadSettings.pending,
saveSettings.pending,
(loading, saving) => loading || saving,
)
export const $isDisabled: Store<boolean> = combine(
$isSettingsReady,
$isLoading,
(ready, loading) => !ready || loading,
)

// Stores for inputs

const defaultAvatar =
"https://www.gravatar.com/avatar/00000000000000000000000000000000?d=mp&s=512"
const avatarParams = "d=retro&rating=g&s=512"

export const $name: Store<string> = createStore("")
export const $avaEmail: Store<string> = createStore("")

export const $avatarUrl: Store<string> = combine(
$settings,
$avaEmail,
(settings, enteredEmail) =>
createUrl(
filterName(enteredEmail) ||
settings?.gravatarEmail ||
settings?.currentEmail ||
defaultAvatar,
),
)

export const $nameChanged: Store<boolean> = combine(
$settings,
$name,
(settings, name) => (settings?.displayName || "") !== filterName(name),
)

export const $avaEmailChanged: Store<boolean> = combine(
$settings,
$avaEmail,
// just remove spaces from email
(settings, email) => (settings?.gravatarEmail || "") !== filterName(email),
)

forward({
from: pageMounted,
to: loadSettings,
})

loadSettings.use(accountApi.getSettings)
saveSettings.use(accountApi.updateSettings)

forward({ from: saveSettings.done, to: loadSession })

$settings
.on(loadSettings.done, (_, { result }) => result.settings)
.on(saveSettings.done, (_, { result }) => result.settings)
.reset(pageMounted, pageUnmounted)

sample({
source: $settings,
clock: saveGravatar,
target: saveSettings,
fn: (settings, gravatarEmail) => ({
displayName: settings?.displayName || "",
gravatarEmail,
}),
})

sample({
source: $settings,
clock: saveName,
target: saveSettings,
fn: (settings, displayName) => ({
gravatarEmail: settings?.gravatarEmail || "",
displayName,
}),
})

$name
.on($settings.updates, (_, settings) => settings?.displayName || "")
.on(nameChanged, (_, event) => event.currentTarget.value)
.reset(pageUnmounted)
.watch(nameSubmitted, (displayName) => {
saveName(filterName(displayName))
})

$avaEmail
.on(
$settings.updates,
(_, settings) => settings?.gravatarEmail || settings?.currentEmail || "",
)
.on(gravatarEmailChanged, (_, event) => event.currentTarget.value)
.reset(pageUnmounted)
.watch(gravatarEmailSubmitted, (gravatarEmail) => {
saveGravatar(gravatarEmail)
})

sample({
source: $settings,
clock: gravatarChangeCancelled,
target: $avaEmail,
fn: (settings) => settings?.gravatarEmail || settings?.currentEmail || "",
})

function createUrl(email) {
return `https://www.gravatar.com/avatar/${md5(email)}?${avatarParams}`
}

function filterName(value) {
return value.trim().replace(/\s+/, " ")
}
118 changes: 105 additions & 13 deletions src/pages/settings/page.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,32 @@ import * as React from "react"
import styled from "styled-components"
import { useStore } from "effector-react"

import { Col, Row } from "@lib/styled-components-layout"
import { SettingsTemplate } from "@features/settings"
import { Button, H4, Input } from "@howtocards/ui"
import { Button, ButtonPrimary, H4, Input, ZeroButton } from "@howtocards/ui"
import {
$avaEmail,
$avaEmailChanged,
$avatarUrl,
$isDisabled,
$name,
$nameChanged,
gravatarChangeCancelled,
gravatarEmailChanged,
gravatarEmailSubmitted,
nameChanged,
nameSubmitted,
pageMounted,
pageUnmounted,
} from "./store"
} from "./model"

export const SettingsPage = () => {
React.useEffect(() => (pageMounted(), pageUnmounted))

return (
<SettingsTemplate title="Personal settings">
<NameSection />
<AvatarSection />
<NameSection />
</SettingsTemplate>
)
}
Expand All @@ -42,28 +49,113 @@ const NameSection = () => {
return (
<FormSection title="Display name">
<form onSubmit={submit}>
<Input
disabled={isDisabled}
label="Enter your name and press Enter"
value={name}
onChange={nameChanged}
/>
<Button disabled={buttonDisabled} type="submit">
Save
</Button>
<Col gap="1rem">
<Input
disabled={isDisabled}
label="Enter your name and press Enter"
value={name}
onChange={nameChanged}
/>
<Row>
<Button disabled={buttonDisabled} type="submit">
Save
</Button>
</Row>
</Col>
</form>
</FormSection>
)
}

const AvatarSection = () => {
const avatarUrl = useStore($avatarUrl)

return (
<FormSection title="Avatar">
Avatar changing form with some logic
<AvatarTemplate>
<Avatar width={140} height={140} src={avatarUrl} />
<ChangeAvatar />
</AvatarTemplate>
</FormSection>
)
}

const ChangeAvatar = () => {
const [opened, setOpened] = React.useState(false)
const gravatarEmail = useStore($avaEmail)
const isDisabled = useStore($isDisabled)
const isChanged = useStore($avaEmailChanged)

const isSaveDisabled = isDisabled || !isChanged

const cancel = React.useCallback(
(event) => {
setOpened(false)
gravatarChangeCancelled(event)
},
[setOpened],
)

const save = React.useCallback((event) => {
event.preventDefault()
gravatarEmailSubmitted(event)
setOpened(false)
}, [])

if (opened) {
return (
<form onSubmit={save} disabled={isSaveDisabled}>
<Col gap="1rem">
<Input
disabled={isDisabled}
label="Enter Email for Gravatar"
value={gravatarEmail}
onChange={gravatarEmailChanged}
/>
<Row gap="1rem">
<ButtonPrimary disabled={isSaveDisabled} type="submit">
{gravatarEmail.trim().length === 0 ? "Use default" : "Save"}
</ButtonPrimary>
<ZeroButton onClick={cancel}>Cancel</ZeroButton>
</Row>
</Col>
</form>
)
}

return (
<Col gap="1rem">
<span>
Gravatar email: <b>{gravatarEmail}</b>
</span>
<Row>
<Button disabled={isDisabled} onClick={() => setOpened(true)}>
Change
</Button>
</Row>
</Col>
)
}

const Avatar = styled.img`
display: block;
border-radius: 3px;
border: 1px solid lightgray;
`

const AvatarTemplate = styled.div`
display: flex;
flex-flow: row nowrap;
${Avatar} {
margin-right: 1rem;
}
form {
flex-grow: 1;
}
`

type FormSectionProps = {
title: string,
children: React.Node,
Expand Down
Loading

0 comments on commit 139d8ff

Please sign in to comment.