Merge pull request #169 from filecoin-project/@martinalong/profiles

In-client profiles / slates + directory tab
This commit is contained in:
martinalong 2020-08-30 22:01:59 -07:00 committed by GitHub
commit efdc156448
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 1273 additions and 608 deletions

View File

@ -71,6 +71,14 @@ export const generate = ({ library = [], slates = [] }) => [
pageTitle: "Slates",
children: constructSlatesTreeForNavigation(slates),
},
{
id: "V1_NAVIGATION_SLATE",
decorator: "VIEW_SLATE",
name: "Slate",
pageTitle: "Slate",
children: null,
ignore: true,
},
constructFilesTreeForNavigation(library),
/*
{
@ -111,7 +119,7 @@ export const generate = ({ library = [], slates = [] }) => [
children: null,
},
{
id: "V1_NAVIGATION_PROFILE",
id: "V1_NAVIGATION_PROFILE_EDIT",
decorator: "EDIT_ACCOUNT",
name: "Profile & Account Settings",
pageTitle: "Your Profile & Account Settings",
@ -126,4 +134,20 @@ export const generate = ({ library = [], slates = [] }) => [
children: null,
ignore: true,
},
{
id: "V1_NAVIGATION_PROFILE",
decorator: "PROFILE",
name: "Profile",
pageTitle: "Profile",
children: null,
ignore: true,
},
{
id: "V1_NAVIGATION_FILE",
decorator: "FILE",
name: "File",
pageTitle: "File",
children: null,
ignore: true,
},
];

View File

@ -23,6 +23,7 @@ import SceneSignIn from "~/scenes/SceneSignIn";
import SceneSlate from "~/scenes/SceneSlate";
import SceneActivity from "~/scenes/SceneActivity";
import SceneDirectory from "~/scenes/SceneDirectory";
import SceneProfile from "~/scenes/SceneProfile";
// NOTE(jim):
// Sidebars each have a decorator and can be shown to with _handleAction
@ -58,6 +59,9 @@ const SIDEBARS = {
const SCENES = {
HOME: <SceneHome />,
DIRECTORY: <SceneDirectory />,
PROFILE: <SceneProfile />,
VIEW_SLATE: <SceneSlate />,
WALLET: <SceneWallet />,
FOLDER: <SceneFilesFolder />,
FILE: <SceneFile />,
@ -73,6 +77,8 @@ const SCENES = {
};
export default class ApplicationPage extends React.Component {
_body;
state = {
selected: {},
viewer: this.props.viewer,
@ -232,7 +238,7 @@ export default class ApplicationPage extends React.Component {
this.setState(updates);
return { rehydrated: true };
return response;
};
_handleSubmit = async (data) => {
@ -347,13 +353,19 @@ export default class ApplicationPage extends React.Component {
};
_handleDismissSidebar = () => {
this.setState({ sidebar: null, sidebarLoading: false, data: null });
this.setState({ sidebar: null, sidebarLoading: false, sidebarData: null });
};
_handleAction = (options) => {
console.log(options);
if (options.type === "NAVIGATE") {
return this._handleNavigateTo({ id: options.value }, options.data);
// NOTE(martina): The `scene` property is only necessary when you need to display a component different from the one corresponding to the tab it appears in
// + e.g. to display <SceneProfile/> while on the Home tab
// + `scene` should be the decorator of the component you want displayed
return this._handleNavigateTo(
{ id: options.value, scene: options.scene },
options.data
);
}
if (options.type === "NEW_WINDOW") {
@ -371,7 +383,7 @@ export default class ApplicationPage extends React.Component {
if (options.type === "SIDEBAR") {
return this.setState({
sidebar: SIDEBARS[options.value],
data: options.data,
sidebarData: options.data,
});
}
@ -379,8 +391,9 @@ export default class ApplicationPage extends React.Component {
};
_handleNavigateTo = (next, data = null) => {
this.state.history[this.state.currentIndex].scrollTop = window.scrollY;
this.state.history[this.state.currentIndex].data = data;
let body = document.getElementById("slate-client-body");
this.state.history[this.state.currentIndex].scrollTop = body.scrollTop; //window.scrollY => body.scrollTop (where body is the body of the ApplicationLayout)
this.state.history[this.state.currentIndex].data = this.state.data; //BUG FIX: was originally = data. So it was setting it equal to the data for the next one rather than the current one
if (this.state.currentIndex !== this.state.history.length - 1) {
const adjustedArray = [...this.state.history];
@ -393,7 +406,7 @@ export default class ApplicationPage extends React.Component {
data,
sidebar: null,
},
() => window.scrollTo(0, 0)
() => body.scrollTo(0, 0)
);
}
@ -404,12 +417,14 @@ export default class ApplicationPage extends React.Component {
data,
sidebar: null,
},
() => window.scrollTo(0, 0)
() => body.scrollTo(0, 0)
);
};
_handleBack = () => {
this.state.history[this.state.currentIndex].scrollTop = window.scrollY;
let body = document.getElementById("slate-client-body");
this.state.history[this.state.currentIndex].scrollTop = body.scrollTop;
this.state.history[this.state.currentIndex].data = this.state.data; //BUG FIX: if you go back, it doesn't save the data for that page. so if you go forward to it again, it breaks. changed data => this.state.data
const next = this.state.history[this.state.currentIndex - 1];
@ -421,13 +436,14 @@ export default class ApplicationPage extends React.Component {
},
() => {
console.log({ next });
window.scrollTo(0, next.scrollTop);
body.scrollTo(0, next.scrollTop);
}
);
};
_handleForward = () => {
this.state.history[this.state.currentIndex].scrollTop = window.scrollY;
let body = document.getElementById("slate-client-body");
this.state.history[this.state.currentIndex].scrollTop = body.scrollTop;
const next = this.state.history[this.state.currentIndex + 1];
@ -439,7 +455,7 @@ export default class ApplicationPage extends React.Component {
},
() => {
console.log({ next });
window.scrollTo(0, next.scrollTop);
body.scrollTo(0, next.scrollTop);
}
);
};
@ -467,6 +483,7 @@ export default class ApplicationPage extends React.Component {
const navigation = NavigationData.generate(this.state.viewer);
const next = this.state.history[this.state.currentIndex];
const current = NavigationData.getCurrentById(navigation, next.id);
console.log(this.state.history);
const navigationElement = (
<ApplicationNavigation
@ -492,20 +509,24 @@ export default class ApplicationPage extends React.Component {
/>
);
const scene = React.cloneElement(SCENES[current.target.decorator], {
current: current.target,
data: this.state.data,
viewer: this.state.viewer,
selected: this.state.selected,
onNavigateTo: this._handleNavigateTo,
onSelectedChange: this._handleSelectedChange,
onViewerChange: this._handleViewerChange,
onDeleteYourself: this._handleDeleteYourself,
onAction: this._handleAction,
onBack: this._handleBack,
onForward: this._handleForward,
onRehydrate: this.rehydrate,
});
const scene = React.cloneElement(
SCENES[next.scene || current.target.decorator],
{
current: current.target,
data: this.state.data,
viewer: this.state.viewer,
selected: this.state.selected,
onNavigateTo: this._handleNavigateTo,
onSelectedChange: this._handleSelectedChange,
onViewerChange: this._handleViewerChange,
onDeleteYourself: this._handleDeleteYourself,
onAction: this._handleAction,
onBack: this._handleBack,
onForward: this._handleForward,
onRehydrate: this.rehydrate,
sceneId: current.target.id,
}
);
let sidebarElement;
if (this.state.sidebar) {

View File

@ -2,7 +2,11 @@ import * as React from "react";
import * as Constants from "~/common/constants";
import * as OldSVG from "~/components/system/svg";
import { TooltipWrapper, dispatchCustomEvent, PopoverNavigation } from "~/components/system";
import {
TooltipWrapper,
dispatchCustomEvent,
PopoverNavigation,
} from "~/components/system";
import { css } from "@emotion/react";
import Dismissible from "~/components/core/Dismissible";
@ -67,7 +71,8 @@ export default class ApplicationControlMenu extends React.Component {
captureScroll={false}
enabled
onOutsideRectEvent={this._handleHide}
style={this.props.style}>
style={this.props.style}
>
<PopoverNavigation
style={{
left: 0,
@ -79,8 +84,8 @@ export default class ApplicationControlMenu extends React.Component {
onSignOut={this._handleSignOut}
navigation={[
{
text: "Account Settings",
value: "V1_NAVIGATION_PROFILE",
text: "Profile & account settings",
value: "V1_NAVIGATION_PROFILE_EDIT",
},
/*
{
@ -92,16 +97,20 @@ export default class ApplicationControlMenu extends React.Component {
]}
/>
</Dismissible>
}>
}
>
<CircleButtonLight
onClick={this._handleClick}
style={{
backgroundColor: this.state.visible ? Constants.system.brand : Constants.system.white,
backgroundColor: this.state.visible
? Constants.system.brand
: Constants.system.white,
color: this.state.visible ? Constants.system.white : null,
}}>
}}
>
<OldSVG.ChevronDown height="20px" />
</CircleButtonLight>
</TooltipWrapper>
</TooltipWrapper>
);
}
}

View File

@ -3,7 +3,7 @@ import * as Constants from "~/common/constants";
import * as SVG from "~/common/svg";
import { css } from "@emotion/react";
import { SpotlightSearch } from "~/components/system/modules/SpotlightSearch";
import { SearchModal } from "~/components/core/SearchModal";
import { dispatchCustomEvent } from "~/common/custom-events";
const STYLES_ICON_ELEMENT = css`
@ -72,7 +72,7 @@ export default class ApplicationHeader extends React.Component {
_handleCreateSearch = (e) => {
dispatchCustomEvent({
name: "create-modal",
detail: { modal: <SpotlightSearch onAction={this.props.onAction} /> },
detail: { modal: <SearchModal onAction={this.props.onAction} /> },
});
};

View File

@ -19,6 +19,7 @@ const IconMap = {
LOCAL_DATA: <SVG.HardDrive height="20px" />,
PROFILE_PAGE: <SVG.ProfileUser height="20px" />,
SETTINGS_DEVELOPER: <SVG.SettingsDeveloper height="20px" />,
DIRECTORY: <SVG.Directory height="20px" />,
};
const STYLES_NAVIGATION = css`
@ -69,6 +70,7 @@ const STYLES_PROFILE = css`
align-items: center;
justify-content: flex-start;
transition: 200ms ease all;
cursor: pointer;
/*
:hover {
color: ${Constants.system.white};
@ -256,11 +258,16 @@ export default class ApplicationNavigation extends React.Component {
return (
<nav css={STYLES_NAVIGATION}>
<div css={STYLES_NAVIGATION_HEADER}>
<a
<div
css={STYLES_PROFILE}
style={{ marginRight: 16 }}
href={`/${this.props.viewer.username}`}
target="_blank"
onClick={() =>
this.props.onAction({
type: "NAVIGATE",
value: "V1_NAVIGATION_PROFILE",
data: this.props.viewer,
})
}
>
<span
css={STYLES_PROFILE_IMAGE}
@ -269,7 +276,7 @@ export default class ApplicationNavigation extends React.Component {
}}
/>
{this.props.viewer.username}
</a>
</div>
<ApplicationControlMenu
onNavigateTo={this.props.onNavigateTo}
onAction={this.props.onAction}

View File

@ -0,0 +1,78 @@
import * as React from "react";
import * as Constants from "~/common/constants";
import { css } from "@emotion/react";
import SlatePreviewBlock from "~/components/core/SlatePreviewBlock";
const STYLES_PROFILE = css`
text-align: center;
width: 100%;
margin-top: 16px;
`;
const STYLES_PROFILE_IMAGE = css`
background-size: cover;
background-position: 50% 50%;
width: 96px;
height: 96px;
border-radius: 50%;
margin: 0 auto;
padding: 0;
`;
const STYLES_NAME = css`
font-size: ${Constants.typescale.lvl3};
margin: 16px 0px;
`;
const STYLES_LINK = css`
color: ${Constants.system.black};
text-decoration: none;
`;
export default class Profile extends React.Component {
render() {
let data = this.props.creator ? this.props.creator : this.props.data;
return (
<div css={STYLES_PROFILE}>
<div
css={STYLES_PROFILE_IMAGE}
style={{ backgroundImage: `url('${data.data.photo}')` }}
/>
<div css={STYLES_NAME}>{data.username}</div>
{this.props.buttons}
<br />
{data.slates && data.slates.length ? (
<div style={{ width: "100%" }}>
{data.slates.map((slate) => {
const url = `/${data.username}/${slate.slatename}`;
if (this.props.onAction) {
return (
<div
key={url}
onClick={() =>
this.props.onAction({
type: "NAVIGATE",
value: this.props.sceneId,
scene: "SLATE",
data: { owner: data, ...slate },
})
}
>
<SlatePreviewBlock slate={slate} />
</div>
);
}
return (
<a key={url} href={url} css={STYLES_LINK}>
<SlatePreviewBlock slate={slate} />
</a>
);
})}
</div>
) : null}
</div>
);
}
}

View File

@ -11,7 +11,6 @@ const STYLES_ROOT = css`
display: flex;
align-items: flex-start;
justify-content: space-between;
max-width: 648px;
width: 100%;
`;
@ -23,6 +22,7 @@ const STYLES_LEFT = css`
const STYLES_RIGHT = css`
flex-shrink: 0;
padding-left: 24px;
justify-self: end;
`;
const STYLES_HEADER = css`

View File

@ -17,9 +17,7 @@ const STYLES_DROPDOWN = css`
background-color: ${Constants.system.white};
overflow: hidden;
width: 100%;
scrollbar-width: none;
-ms-overflow-style: -ms-autohiding-scrollbar;
::-webkit-scrollbar {
display: none;
@ -32,8 +30,10 @@ const STYLES_DROPDOWN_ITEM = css`
font-size: 0.8em;
border-radius: 16px;
border: 1px solid ${Constants.system.white};
cursor: pointer;
:hover {
border-color: ${Constants.system.border};
border-color: ${Constants.system.border} !important;
}
`;
@ -72,10 +72,14 @@ const STYLES_INPUT = css`
}
`;
export class InputMenu extends React.Component {
export class SearchDropdown extends React.Component {
_input;
_optionRoot;
static defaultProps = {
defaultResults: [],
};
state = {
selectedIndex: -1,
};
@ -89,27 +93,15 @@ export class InputMenu extends React.Component {
window.removeEventListener("keydown", this._handleDocumentKeydown);
};
_handleInputChange = (e) => {
_handleChange = (e) => {
if (this.state.selectedIndex !== -1) {
this.setState({ selectedIndex: -1 });
}
this.props.onChange({
target: {
value: null,
name: this.props.name,
},
});
this.props.onInputChange(e);
this.props.onChange(e);
};
_handleSelect = (index) => {
let e = {
target: {
value: this.props.options[index].value,
name: this.props.name,
},
};
this.props.onChange(e);
this.props.onSelect(this.props.results[index].value);
};
_handleDocumentKeydown = (e) => {
@ -119,7 +111,7 @@ export class InputMenu extends React.Component {
} else if (e.keyCode === 9) {
this._handleDelete();
} else if (e.keyCode === 40) {
if (this.state.selectedIndex < this.props.options.length - 1) {
if (this.state.selectedIndex < this.props.results.length - 1) {
let listElem = this._optionRoot.children[this.state.selectedIndex + 1];
let elemRect = listElem.getBoundingClientRect();
let rootRect = this._optionRoot.getBoundingClientRect();
@ -145,7 +137,7 @@ export class InputMenu extends React.Component {
e.preventDefault();
} else if (e.keyCode === 13) {
if (
this.props.options.length > this.state.selectedIndex &&
this.props.results.length > this.state.selectedIndex &&
this.state.selectedIndex !== -1
) {
this._handleSelect(this.state.selectedIndex);
@ -163,7 +155,7 @@ export class InputMenu extends React.Component {
value={this.props.inputValue}
placeholder={this.props.placeholder}
style={this.props.inputStyle}
onChange={this._handleInputChange}
onChange={this._handleChange}
ref={(c) => {
this._input = c;
}}
@ -182,22 +174,23 @@ export class InputMenu extends React.Component {
css={STYLES_DROPDOWN}
style={this.props.style}
>
{(this.props.options && this.props.options.length
? this.props.options
: this.props.defaultOptions
{(this.props.results && this.props.results.length
? this.props.results
: this.props.defaultResults
).map((each, i) => (
<div
key={each.value}
key={each.value.data.id}
css={STYLES_DROPDOWN_ITEM}
style={{
borderColor:
this.state.selectedIndex === i
? Constants.system.border
: "auto",
: Constants.system.white,
...this.props.itemStyle,
}}
onClick={() => this.props.onSelect(each.value)}
>
{each.name}
{each.component}
</div>
))}
</div>

View File

@ -0,0 +1,309 @@
import * as React from "react";
import * as Constants from "~/common/constants";
import * as SVG from "~/common/svg";
import * as Strings from "~/common/strings";
import * as Actions from "~/common/actions";
import MiniSearch from "minisearch";
import SlateMediaObjectPreview from "~/components/core/SlateMediaObjectPreview";
import { css } from "@emotion/react";
import { SearchDropdown } from "~/components/core/SearchDropdown";
import { dispatchCustomEvent } from "~/common/custom-events";
import { SlatePreviewRow } from "~/components/core/SlatePreviewBlock";
import { LoaderSpinner } from "~/components/system/components/Loaders";
const STYLES_ICON_CIRCLE = css`
height: 24px;
width: 24px;
border-radius: 50%;
background-color: ${Constants.system.foreground};
display: flex;
align-items: center;
justify-content: center;
`;
const STYLES_MODAL = css`
width: 95vw;
max-width: 600px;
height: 60vh;
max-height: 500px;
padding: 24px;
`;
const STYLES_SEARCH_DROPDOWN = {
height: "calc(100% - 80px)",
width: "calc(100% - 48px)",
overflowY: "scroll",
};
const STYLES_USER_ENTRY_CONTAINER = css`
display: grid;
grid-template-columns: repeat(3, auto) 1fr;
grid-column-gap: 16px;
align-items: center;
`;
const STYLES_PROFILE_IMAGE = css`
background-size: cover;
background-position: 50% 50%;
height: 24px;
width: 24px;
border-radius: 50%;
`;
const UserEntry = ({ item }) => {
return (
<div css={STYLES_ENTRY}>
<div css={STYLES_USER_ENTRY_CONTAINER}>
<div
style={{ backgroundImage: `url(${item.data.photo})` }}
css={STYLES_PROFILE_IMAGE}
/>
{item.data.name ? <strong>{item.data.name}</strong> : null}
<div>@{item.username}</div>
</div>
</div>
);
};
const STYLES_ENTRY = css`
padding: 8px 0px;
`;
const STYLES_SLATE_ENTRY_CONTAINER = css`
display: grid;
grid-template-columns: repeat(3, auto) 1fr;
grid-column-gap: 16px;
align-items: center;
`;
const STYLES_SLATE_IMAGES_CONTAINER = css`
margin-left: 32px;
`;
const STYLES_SLATE_IMAGE = css`
display: flex;
align-items: center;
justify-content: center;
height: 72px;
width: 72px;
padding: 8px;
box-sizing: border-box;
margin-left: 32px;
`;
const STYLES_LINK_HOVER = css`
color: ${Constants.system.black};
:hover {
color: ${Constants.system.brand};
}
`;
const SlateEntry = ({ item }) => {
return (
<div css={STYLES_ENTRY}>
<div css={STYLES_SLATE_ENTRY_CONTAINER}>
<div css={STYLES_ICON_CIRCLE}>
<SVG.Slate2 height="16px" />
</div>
<strong>{item.data.name}</strong>
<div>@{item.owner.username}</div>
</div>
{item.data.objects.length ? (
<div css={STYLES_SLATE_IMAGES_CONTAINER}>
<SlatePreviewRow
numItems={4}
style={{ width: "72px", height: "72px", padding: "8px" }}
containerStyle={{
maxHeight: "72px",
justifyContent: "flex-start",
}}
previewStyle={{ fontSize: "12px", padding: "4px" }}
slate={item}
/>
</div>
) : null}
</div>
);
};
const FileEntry = ({ item }) => {
return (
<div css={STYLES_ENTRY}>
<div css={STYLES_USER_ENTRY_CONTAINER}>
<div css={STYLES_ICON_CIRCLE}>
<SVG.Folder2 height="16px" />
</div>
<strong>{item.data.file.title || item.data.file.name}</strong>
<div css={STYLES_LINK_HOVER}>@{item.data.slate.owner.username}</div>
</div>
<div css={STYLES_SLATE_IMAGE}>
<SlateMediaObjectPreview
style={{ fontSize: "12px", padding: "4px" }}
url={item.data.file.url}
type={item.type}
/>
</div>
</div>
);
};
const STYLES_LOADER = css`
position: absolute;
top: calc(50% - 22px);
left: calc(50% - 22px);
`;
export class SearchModal extends React.Component {
state = {
loading: true,
results: [],
inputValue: "",
};
componentDidMount = async () => {
await this.fillDirectory();
this.setState({ loading: false });
};
fillDirectory = async () => {
const response = await Actions.getNetworkDirectory();
this.miniSearch = new MiniSearch({
fields: ["slatename", "data.name", "username", "filename"],
storeFields: [
"type",
"slatename",
"username",
"data",
"id",
"slates",
"owner",
],
extractField: (entry, fieldName) => {
return fieldName
.split(".")
.reduce((doc, key) => doc && doc[key], entry);
},
searchOptions: {
fuzzy: 0.2,
},
});
let files = [];
if (response.data) {
for (let slate of response.data.slates) {
if (slate.data.objects.length) {
files.push(
...slate.data.objects.map((file, i) => {
return {
type: "FILE",
id: file.id,
filename: file.title,
data: { file, index: i, slate },
};
})
);
}
}
this.users = response.data.users;
this.slates = response.data.slates;
this.miniSearch.addAll(response.data.users);
this.miniSearch.addAll(response.data.slates);
this.miniSearch.addAll(files);
}
};
_handleChange = (e) => {
if (!this.state.loading) {
this.setState({ inputValue: e.target.value }, () => {
let searchResults = this.miniSearch.search(this.state.inputValue);
let results = [];
for (let item of searchResults) {
if (item.type === "USER") {
results.push({
value: {
type: "USER",
data: item,
},
component: <UserEntry item={item} />,
});
} else if (item.type === "SLATE") {
results.push({
value: {
type: "SLATE",
data: item,
},
component: (
<SlateEntry item={item} onAction={this.props.onAction} />
),
});
} else if (item.type === "FILE") {
results.push({
value: {
type: "FILE",
data: item,
},
component: <FileEntry item={item} />,
});
}
this.setState({ results });
}
});
}
};
_handleSelect = async (value) => {
if (value.type === "SLATE") {
value.data.owner = this.users.filter((user) => {
return user.username === value.data.owner.username;
})[0]; //TODO: slightly hacky way of getting the data. May want to serialize later?
this.props.onAction({
type: "NAVIGATE",
value: "V1_NAVIGATION_SLATE",
data: value.data,
});
}
if (value.type === "USER") {
this.props.onAction({
type: "NAVIGATE",
value: "V1_NAVIGATION_PROFILE",
data: value.data,
});
}
if (value.type === "FILE") {
let slate = value.data.data.slate;
slate.owner = this.users.filter((user) => {
return user.username === slate.owner.username;
})[0]; //TODO: slightly hacky way of getting the data. May want to serialize later?
this.props.onAction({
type: "NAVIGATE",
value: "V1_NAVIGATION_SLATE",
data: slate,
});
dispatchCustomEvent({
name: "slate-global-open-carousel",
detail: { index: value.data.data.index },
});
}
dispatchCustomEvent({
name: "delete-modal",
detail: {},
});
};
render() {
return (
<div css={STYLES_MODAL}>
<SearchDropdown
placeholder="Search..."
results={this.state.results}
onSelect={this._handleSelect}
onChange={this._handleChange}
inputValue={this.state.inputValue}
style={STYLES_SEARCH_DROPDOWN}
/>
{this.state.loading ? <LoaderSpinner css={STYLES_LOADER} /> : null}
</div>
);
}
}

View File

@ -30,26 +30,48 @@ export default class SlateMediaObjectPreview extends React.Component {
// This is a hack to catch this undefined case I don't want to track down yet.
const url = this.props.url.replace("https://undefined", "https://");
let element = <article css={STYLES_ENTITY}>No Preview</article>;
let element = (
<article css={STYLES_ENTITY} style={this.props.style}>
No Preview
</article>
);
if (this.props.type && this.props.type.startsWith("video/")) {
element = <article css={STYLES_ENTITY}>Video</article>;
element = (
<article css={STYLES_ENTITY} style={this.props.style}>
Video
</article>
);
}
if (this.props.type && this.props.type.startsWith("audio/")) {
element = <article css={STYLES_ENTITY}>Audio</article>;
element = (
<article css={STYLES_ENTITY} style={this.props.style}>
Audio
</article>
);
}
if (this.props.type && this.props.type.startsWith("application/epub")) {
element = <article css={STYLES_ENTITY}>EPub</article>;
element = (
<article css={STYLES_ENTITY} style={this.props.style}>
EPub
</article>
);
}
if (this.props.type && this.props.type.startsWith("application/pdf")) {
element = <article css={STYLES_ENTITY}>PDF</article>;
element = (
<article css={STYLES_ENTITY} style={this.props.style}>
PDF
</article>
);
}
if (this.props.type && this.props.type.startsWith("image/")) {
element = <img css={STYLES_IMAGE} src={url} />;
element = (
<img css={STYLES_IMAGE} style={this.props.imageStyle} src={url} />
);
}
return element;

View File

@ -160,7 +160,7 @@ export default class SlateMediaObjectSidebar extends React.Component {
const elements = [];
if (this.props.data) {
if (this.props.onObjectSave) {
if (this.props.editing) {
elements.push(
<React.Fragment key="sidebar-media-object-info">
<div css={STYLES_SIDEBAR_SECTION}>
@ -300,30 +300,29 @@ export default class SlateMediaObjectSidebar extends React.Component {
if (this.props.cid) {
elements.push(
<a
key="sidebar-media-open-file"
css={STYLES_BUTTON}
href={Strings.getCIDGatewayURL(this.props.cid)}
target="_blank"
>
Open file in a new browser tab &nbsp;&nbsp;
</a>
);
elements.push(
<a
key="sidebar-media-download-file"
css={STYLES_BUTTON}
href={Strings.getCIDGatewayURL(this.props.cid)}
target="_blank"
download={this.props.cid}
>
Download file &nbsp;&nbsp;
</a>
<div css={STYLES_SIDEBAR} style={{ height: "auto" }}>
<a
key="sidebar-media-open-file"
css={STYLES_BUTTON}
href={Strings.getCIDGatewayURL(this.props.cid)}
target="_blank"
>
Open file in new tab &nbsp;&nbsp;
</a>
<a
key="sidebar-media-download-file"
css={STYLES_BUTTON}
href={Strings.getCIDGatewayURL(this.props.cid)}
target="_blank"
download={this.props.cid}
>
Download file &nbsp;&nbsp;
</a>
</div>
);
}
if (this.props.onDelete) {
if (this.props.onDelete && this.props.editing) {
elements.push(
<span
key="sidebar-media-object-delete"
@ -333,7 +332,7 @@ export default class SlateMediaObjectSidebar extends React.Component {
{this.props.loading ? (
<LoaderSpinner style={{ height: 16, width: 16 }} />
) : (
<span>Delete Slate object&nbsp;&nbsp;&nbsp;</span>
<span>Delete from slate&nbsp;&nbsp;&nbsp;</span>
)}
</span>
);

View File

@ -0,0 +1,75 @@
//doable with vw. calculate vw - sidebar (if mobile size), divided by numItems = max-width and max-height
import React from "react";
import * as Constants from "~/common/constants";
import { css } from "@emotion/react";
import SlateMediaObjectPreview from "~/components/core/SlateMediaObjectPreview";
const STYLES_IMAGE_ROW = css`
display: flex;
flex-direction: row;
flex-wrap: wrap;
max-height: 186px;
overflow: hidden;
@media (max-width: ${Constants.sizes.mobile}px) {
justify-content: center;
}
`;
const STYLES_ITEM_BOX = css`
width: 186px;
height: 186px;
padding: 16px;
display: flex;
align-items: center;
justify-content: center;
`;
export function SlatePreviewRow(props) {
let numItems = props.numItems || 5;
let objects =
props.slate.data.objects.length > numItems
? props.slate.data.objects.slice(0, numItems)
: props.slate.data.objects;
return (
<div css={STYLES_IMAGE_ROW} style={props.containerStyle}>
{objects.map((each) => (
<div key={each.url} css={STYLES_ITEM_BOX} style={props.style}>
<SlateMediaObjectPreview
type={each.type}
url={each.url}
style={props.previewStyle}
/>
</div>
))}
</div>
);
}
const STYLES_BLOCK = css`
border: 1px solid ${Constants.system.border};
border-radius: 16px;
padding: 24px;
font-size: 12px;
text-align: left;
margin: 24px auto;
width: 95%;
max-width: 980px;
cursor: pointer;
`;
const STYLES_SLATE_NAME = css`
font-size: ${Constants.typescale.lvl1};
`;
export default function SlatePreviewBlock(props) {
return (
<div css={STYLES_BLOCK}>
<div css={STYLES_SLATE_NAME}>{props.slate.data.name}</div>
<SlatePreviewRow {...props} previewStyle={props.previewStyle} />
</div>
);
}

View File

@ -16,7 +16,6 @@ import {
FilecoinRetrievalDealsList,
} from "~/components/system/modules/FilecoinDealsList";
import { FilecoinSettings } from "~/components/system/modules/FilecoinSettings";
import { SpotlightSearch } from "~/components/system/modules/SpotlightSearch";
// NOTE(jim): Global components
import { GlobalModal } from "~/components/system/components/GlobalModal";
@ -37,7 +36,6 @@ import { CheckBox } from "~/components/system/components/CheckBox";
import { CodeTextarea } from "~/components/system/components/CodeTextarea";
import { DatePicker } from "~/components/system/components/DatePicker";
import { Input } from "~/components/system/components/Input";
import { InputMenu } from "~/components/system/components/InputMenu";
import { ListEditor } from "~/components/system/components/ListEditor";
import { HoverTile } from "~/components/system/components/HoverTile";
import { PopoverNavigation } from "~/components/system/components/PopoverNavigation";
@ -121,7 +119,6 @@ export {
GlobalCarousel,
GlobalNotification,
Input,
InputMenu,
HoverTile,
ListEditor,
PopoverNavigation,
@ -129,7 +126,6 @@ export {
SelectCountryMenu,
SelectMenu,
Slider,
SpotlightSearch,
StatUpload,
StatDownload,
TabGroup,

View File

@ -1,329 +0,0 @@
import * as React from "react";
import * as Constants from "~/common/constants";
import * as SVG from "~/common/svg";
import * as Strings from "~/common/strings";
import * as Actions from "~/common/actions";
import MiniSearch from "minisearch";
import SlateMediaObjectPreview from "~/components/core/SlateMediaObjectPreview";
import { css } from "@emotion/react";
import { InputMenu } from "~/components/system/components/InputMenu";
import { dispatchCustomEvent } from "~/common/custom-events";
const STYLES_ICON_CIRCLE = css`
height: 24px;
width: 24px;
border-radius: 50%;
background-color: ${Constants.system.foreground};
display: flex;
align-items: center;
justify-content: center;
`;
const STYLES_MODAL = css`
width: 95vw;
max-width: 600px;
height: 60vh;
max-height: 500px;
padding: 24px;
`;
const STYLES_INPUT_MENU = {
height: "calc(100% - 80px)",
width: "calc(100% - 48px)",
overflowY: "scroll",
};
const STYLES_USER_ENTRY_CONTAINER = css`
display: grid;
grid-template-columns: repeat(3, auto) 1fr;
grid-column-gap: 16px;
align-items: center;
`;
const STYLES_PROFILE_IMAGE = css`
background-size: cover;
background-position: 50% 50%;
height: 24px;
width: 24px;
border-radius: 50%;
`;
const UserEntry = ({ item }) => {
return (
<a css={STYLES_LINK} href={`/${item.username}`}>
<div css={STYLES_ENTRY}>
<div css={STYLES_USER_ENTRY_CONTAINER}>
<div
style={{ backgroundImage: `url(${item.data.photo})` }}
css={STYLES_PROFILE_IMAGE}
/>
{item.data.name ? <strong>{item.data.name}</strong> : null}
<a css={STYLES_LINK_HOVER} href={`/${item.username}`}>
@{item.username}
</a>
</div>
</div>
</a>
);
};
const STYLES_ENTRY = css`
padding: 8px 0px;
`;
const STYLES_SLATE_ENTRY_CONTAINER = css`
display: grid;
grid-template-columns: repeat(3, auto) 1fr;
grid-column-gap: 16px;
align-items: center;
`;
const STYLES_SLATE_IMAGES_CONTAINER = css`
display: grid;
grid-template-columns: repeat(3, auto) 1fr;
grid-column-gap: 16px;
margin: 8px 0px;
margin-left: 40px;
`;
const STYLES_SLATE_IMAGE = css`
display: flex;
align-items: center;
justify-content: center;
height: 72px;
width: 72px;
`;
const STYLES_LINK = css`
color: ${Constants.system.black};
text-decoration: none;
`;
const STYLES_LINK_HOVER = css`
color: ${Constants.system.black};
text-decoration: none;
:hover {
color: ${Constants.system.brand};
}
`;
const SlateEntry = ({ item, onAction }) => {
//TODO: utilize auto suggest feature of minisearch
return (
<div
// onClick={() => {
// onAction({ type: "NAVIGATE", value: 17, data: item });
// }}
>
<div css={STYLES_ENTRY}>
<div css={STYLES_SLATE_ENTRY_CONTAINER}>
<div css={STYLES_ICON_CIRCLE}>
<SVG.Slate2 height="16px" />
</div>
<strong>{item.data.name}</strong>
{/* <div>
<a css={STYLES_LINK_HOVER} href={`/${item.username}`}>
@{item.data.username} TODO: add the owner to the slate entries
</a>
</div> */}
</div>
{item.data.objects.length ? (
<div css={STYLES_SLATE_IMAGES_CONTAINER}>
{item.data.objects.slice(0, 4).map((each) => (
<div css={STYLES_SLATE_IMAGE}>
<SlateMediaObjectPreview type={each.type} url={each.url} />
</div>
))}
</div>
) : null}
</div>
</div>
);
};
const FileEntry = ({ item, onAction }) => {
return (
<div
css={STYLES_LINK}
// onClick={() => {
// onAction({ type: "NAVIGATE", value: 15, data: { url: item.url } });
// }}
>
<div css={STYLES_ENTRY}>
<div css={STYLES_USER_ENTRY_CONTAINER}>
<div css={STYLES_ICON_CIRCLE}>
<SVG.Folder2 height="16px" />
</div>
<strong>{item.name}</strong>
<a href={`/${item.username}`} css={STYLES_LINK_HOVER}>
@{item.username}
</a>
</div>
<div
style={{
backgroundImage: `url(${
item.type === "image" ? item.url : fileImg
})`,
margin: "8px 0px 8px 40px",
}}
css={STYLES_SLATE_IMAGE}
/>
</div>
</div>
);
};
const STYLES_DROPDOWN_ITEM = css`
display: grid;
grid-template-columns: 56px 1fr;
align-items: center;
cursor: pointer;
`;
const options = [
{
name: "Send money",
link: null,
icon: <SVG.Wallet2 height="16px" />,
action: { type: "NAVIGATE", value: 2 },
},
{
name: "New slate",
link: null,
icon: <SVG.Slate2 height="16px" />,
action: { type: "NAVIGATE", value: 3 },
},
{
name: "Upload file",
link: null,
icon: <SVG.Folder2 height="16px" />,
action: { type: "NAVIGATE", value: "data" },
},
{
name: "Account settings",
link: null,
icon: <SVG.Tool2 height="16px" />,
action: { type: "NAVIGATE", value: 13 },
},
{
name: "Filecoin settings",
link: null,
icon: <SVG.Tool2 height="16px" />,
action: { type: "NAVIGATE", value: 14 },
},
];
export class SpotlightSearch extends React.Component {
state = {
options: [],
value: null,
inputValue: "",
};
componentDidMount = async () => {
const response = await Actions.getNetworkDirectory();
console.log(response.data);
this.miniSearch = new MiniSearch({
fields: ["slatename", "data.name", "username"], // fields to index for full-text search
storeFields: ["type", "slatename", "username", "data", "id"], // fields to return with search results
extractField: (entry, fieldName) => {
return fieldName
.split(".")
.reduce((doc, key) => doc && doc[key], entry); // Access nested fields
},
searchOptions: {
// boost: { "data.name": 2 },
fuzzy: 0.2,
},
});
this.miniSearch.addAll(response.data.users);
this.miniSearch.addAll(response.data.slates);
//TODO: unpack slates => slate object files and add those too
};
_handleChange = (e) => {
// if (e.target.value !== null) {
// if (e.target.value.substring(0, 1) === "/") {
// window.location.pathname = e.target.value;
// } else {
// window.location.href = e.target.value;
// }
// }
};
_handleInputChange = (e) => {
this.setState({ inputValue: e.target.value }, () => {
let results = this.miniSearch.search(this.state.inputValue);
let options = [];
for (let item of results) {
if (item.type === "USER") {
options.push({
value: `/${item.username}`,
name: <UserEntry item={item} onAction={this.props.onAction} />,
});
} else if (item.type === "SLATE") {
options.push({
value: `/${item.slatename}`, //change this format for input menu to something more appropriate
name: <SlateEntry item={item} onAction={this.props.onAction} />,
});
}
// else if (item.type === "image" || item.type == "file") {
// options.push({
// value: `${item.url}`,
// name: <FileEntry item={item} onAction={this.props.onAction} />,
// });
// }
}
this.setState({ options });
});
};
_handleAction = (action) => {
this.props.onAction(action);
dispatchCustomEvent({
name: "delete-modal",
detail: {},
});
};
render() {
return (
<div css={STYLES_MODAL}>
<InputMenu
show
search
name="exampleThree"
placeholder="Search..."
options={this.state.options}
onChange={this._handleChange}
value={this.state.value}
onInputChange={this._handleInputChange}
inputValue={this.state.inputValue}
style={STYLES_INPUT_MENU}
defaultOptions={[]}
// defaultOptions={options.map((option) => {
// return {
// name: (
// <div
// css={STYLES_DROPDOWN_ITEM}
// onClick={() => this._handleAction(option.action)}
// >
// <div
// css={STYLES_ICON_CIRCLE}
// style={{ height: "40px", width: "40px" }}
// >
// {option.icon}
// </div>
// <div>{option.name}</div>
// </div>
// ),
// value: option.name,
// };
// })}
/>
</div>
);
}
}

View File

@ -48,7 +48,7 @@ export const getById = async ({ id }) => {
// NOTE(jim): This should be the cheapest call.
const pendingTrusted = await Data.getPendingTrustedRelationshipsByUserId({
userId: [],
userId: id,
});
const r3 = await Serializers.doPendingTrusted({
users: [id],

View File

@ -67,6 +67,7 @@ export default class IntegrationPage extends React.Component {
_handleDelete = async (entity) => {
const response = await Actions.deleteTrustRelationship({ id: entity.id });
console.log(response);
await this._handleUpdate();
};

View File

@ -2,6 +2,7 @@ import * as React from "react";
import * as Constants from "~/common/constants";
import { css } from "@emotion/react";
import Profile from "~/components/core/Profile";
import WebsitePrototypeWrapper from "~/components/core/WebsitePrototypeWrapper";
import WebsitePrototypeHeader from "~/components/core/WebsitePrototypeHeader";
@ -14,67 +15,11 @@ export const getServerSideProps = async (context) => {
};
const STYLES_ROOT = css`
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
height: 100vh;
display: grid;
grid-template-rows: auto 1fr auto;
text-align: center;
font-size: 1rem;
`;
const STYLES_MIDDLE = css`
position: relative;
min-height: 10%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
padding: 24px;
`;
const STYLES_CARD = css`
margin: 0 auto 0 auto;
max-width: 360px;
width: 100%;
background: ${Constants.system.pitchBlack};
border-radius: 8px;
padding: 0;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.7);
`;
const STYLES_CARD_IMAGE = css`
display: block;
width: 100%;
border-radius: 8px 8px 8px 8px;
margin: 0;
padding: 0;
`;
const STYLES_CARD_PARAGRAPH = css`
font-family: ${Constants.font.code};
padding: 24px;
font-size: 12px;
text-transform: uppercase;
text-align: left;
color: ${Constants.system.white};
`;
const STYLES_LINK = css`
color: ${Constants.system.white};
text-decoration: none;
transition: 200ms ease color;
display: block;
margin-top: 4px;
:visited {
color: ${Constants.system.white};
}
:hover {
color: ${Constants.system.brand};
}
min-height: 100vh;
`;
export default class ProfilePage extends React.Component {
@ -92,26 +37,9 @@ export default class ProfilePage extends React.Component {
>
<div css={STYLES_ROOT}>
<WebsitePrototypeHeader />
<div css={STYLES_CARD}>
<img css={STYLES_CARD_IMAGE} src={this.props.creator.data.photo} />
{this.props.creator.slates && this.props.creator.slates.length ? (
<p css={STYLES_CARD_PARAGRAPH}>
{this.props.creator.slates.map((row) => {
const url = `/${this.props.creator.username}/${
row.slatename
}`;
return (
<a key={url} css={STYLES_LINK} href={url}>
{url}
</a>
);
})}
</p>
) : null}
<div style={{ marginTop: "80px" }}>
<Profile {...this.props} />
</div>
<WebsitePrototypeFooter />
</div>
</WebsitePrototypeWrapper>

View File

@ -110,6 +110,7 @@ export default class SlatePage extends React.Component {
return {
id: each.id,
data: each,
editing: false,
component: (
<SlateMediaObject key={each.id} useImageFallback data={each} />
),
@ -126,9 +127,7 @@ export default class SlatePage extends React.Component {
});
render() {
const title = `${this.props.creator.username}/${
this.props.slate.slatename
}`;
const title = `${this.props.creator.username}/${this.props.slate.slatename}`;
const url = `https://slate.host/${this.props.creator.username}`;
const description = this.props.slate.data.body;
@ -159,7 +158,7 @@ export default class SlatePage extends React.Component {
</WebsitePrototypeHeaderGeneric>
<div css={STYLES_SLATE}>
<Slate
editable={false}
editing={false}
layouts={this.state.layouts}
items={this.props.slate.data.objects}
onSelect={this._handleSelect}

View File

@ -1,19 +1,285 @@
import * as React from "react";
import * as System from "~/components/system";
import * as Actions from "~/common/actions";
import * as Constants from "~/common/constants";
import * as SVG from "~/components/system/svg";
import { css } from "@emotion/react";
import ScenePage from "~/components/core/ScenePage";
import ScenePageHeader from "~/components/core/ScenePageHeader";
// TODO(jim): Figure out the activity story.
const STYLES_TAB = css`
padding: 8px 8px 8px 0px;
margin-right: 24px;
cursor: pointer;
display: inline-block;
font-size: ${Constants.typescale.lvl2};
@media (max-width: ${Constants.sizes.mobile}px) {
font-size: ${Constants.typescale.lvl1};
margin-right: 12px;
}
`;
const STYLES_TAB_GROUP = css`
border-bottom: 1px solid ${Constants.system.gray};
margin: 24px 0px 24px 0px;
`;
const STYLES_USER_ENTRY = css`
display: grid;
grid-template-columns: 1fr auto;
align-items: center;
font-size: ${Constants.typescale.lvl1};
cursor: pointer;
`;
const STYLES_USER = css`
display: grid;
grid-template-columns: auto 1fr;
align-items: center;
`;
const STYLES_ACTION_BUTTON = css`
padding: 8px;
cursor: pointer;
justify-self: end;
:hover {
color: ${Constants.system.brand};
}
`;
const STYLES_PROFILE_IMAGE = css`
background-size: cover;
background-position: 50% 50%;
height: 24px;
width: 24px;
margin: 8px 24px 8px 8px;
border-radius: 50%;
`;
function UserEntry({ user, button, onClick }) {
return (
<div key={user.username} css={STYLES_USER_ENTRY}>
<div css={STYLES_USER} onClick={onClick}>
<div
css={STYLES_PROFILE_IMAGE}
style={{ backgroundImage: `url(${user.data.photo})` }}
/>
<div>@{user.username}</div>
</div>
{button}
</div>
);
}
export default class SceneDirectory extends React.Component {
state = {
loading: false,
tab: "requests",
viewer: this.props.viewer,
};
_handleUpdate = async (e) => {
let response = await this.props.onRehydrate();
// const response = await Actions.hydrateAuthenticatedUser();
if (!response || response.error) {
alert("TODO: error fetching authenticated viewer");
return null;
}
let viewer = response.data;
this.setState({ viewer });
};
_handleDelete = async (id) => {
const response = await Actions.deleteTrustRelationship({
id: id,
});
await this._handleUpdate();
};
_handleAccept = async (id) => {
const response = await Actions.updateTrustRelationship({
userId: id,
});
await this._handleUpdate();
};
_handleFollow = async (id) => {
const response = await Actions.createSubscription({
userId: id,
});
await this._handleUpdate();
};
render() {
return (
<ScenePage>
<ScenePageHeader title="Directory [WIP]">
This scene is currently a work in progress.
</ScenePageHeader>
<ScenePageHeader title="Directory" />
<div css={STYLES_TAB_GROUP}>
<div
css={STYLES_TAB}
style={{
color:
this.state.tab === "requests"
? Constants.system.pitchBlack
: Constants.system.gray,
}}
onClick={() => this.setState({ tab: "requests" })}
>
Requests
</div>
<div
css={STYLES_TAB}
style={{
color:
this.state.tab === "peers"
? Constants.system.pitchBlack
: Constants.system.gray,
}}
onClick={() => this.setState({ tab: "peers" })}
>
Trusted
</div>
<div
css={STYLES_TAB}
style={{
marginRight: "0px",
color:
this.state.tab === "following"
? Constants.system.pitchBlack
: Constants.system.gray,
}}
onClick={() => this.setState({ tab: "following" })}
>
Following
</div>
</div>
{this.state.tab === "requests"
? this.state.viewer.pendingTrusted
.filter((relation) => {
return !relation.data.verified;
})
.map((relation) => {
let button = (
<div
css={STYLES_ACTION_BUTTON}
onClick={() => this._handleAccept(relation.owner.id)}
>
Accept
</div>
);
return (
<UserEntry
user={relation.owner}
button={button}
onClick={() => {
this.props.onAction({
type: "NAVIGATE",
value: this.props.sceneId,
scene: "PROFILE",
data: relation.owner,
});
}}
/>
);
})
: null}
{this.state.tab === "peers" ? (
<div>
{this.state.viewer.pendingTrusted
.filter((relation) => {
return relation.data.verified;
})
.map((relation) => {
let button = (
<div
css={STYLES_ACTION_BUTTON}
onClick={() => this._handleDelete(relation.id)}
>
Remove
</div>
);
return (
<UserEntry
user={relation.owner}
button={button}
onClick={() => {
this.props.onAction({
type: "NAVIGATE",
value: this.props.sceneId,
scene: "PROFILE",
data: relation.owner,
});
}}
/>
);
})}
{this.state.viewer.trusted
.filter((relation) => {
return relation.data.verified;
})
.map((relation) => {
let button = (
<div
css={STYLES_ACTION_BUTTON}
onClick={() => this._handleDelete(relation.id)}
>
Remove
</div>
);
return (
<UserEntry
user={relation.user}
button={button}
onClick={() => {
this.props.onAction({
type: "NAVIGATE",
value: this.props.sceneId,
scene: "PROFILE",
data: relation.user,
});
}}
/>
);
})}
</div>
) : null}
{this.state.tab === "following"
? this.state.viewer.subscriptions
.filter((relation) => {
return !!relation.target_user_id;
})
.map((relation) => {
let button = (
<div
css={STYLES_ACTION_BUTTON}
onClick={() => this._handleFollow(relation.user.id)}
>
Unfollow
</div>
);
return (
<UserEntry
user={relation.user}
button={button}
onClick={() => {
this.props.onAction({
type: "NAVIGATE",
value: this.props.sceneId,
scene: "PROFILE",
data: relation.user,
});
}}
/>
);
})
: null}
</ScenePage>
);
}

198
scenes/SceneProfile.js Normal file
View File

@ -0,0 +1,198 @@
import * as React from "react";
import * as System from "~/components/system";
import * as Actions from "~/common/actions";
import * as Constants from "~/common/constants";
import * as SVG from "~/components/system/svg";
import { css } from "@emotion/react";
import { ButtonPrimary } from "~/components/system/components/Buttons";
import ScenePage from "~/components/core/ScenePage";
import Profile from "~/components/core/Profile";
import CircleButtonLight from "~/components/core/CircleButtonLight";
const BUTTON_STYLES = {
border: `1px solid ${Constants.system.border}`,
boxShadow: "none",
fontFamily: Constants.font.text,
margin: "8px",
padding: "8px 16px",
minHeight: "30px",
};
const BUTTON_SECONDARY_STYLE = {
...BUTTON_STYLES,
backgroundColor: Constants.system.white,
color: Constants.system.brand,
};
const BUTTON_PRIMARY_STYLE = {
...BUTTON_STYLES,
backgroundColor: Constants.system.brand,
color: Constants.system.white,
};
const STATUS_BUTTON_MAP = {
trusted: "Remove peer",
untrusted: "Add peer",
sent: "Cancel request",
received: "Accept request",
};
export default class SceneProfile extends React.Component {
state = {
loading: false,
trustStatus: "untrusted",
followStatus: false,
};
componentDidMount = () => {
this.setStatus(this.props.viewer);
};
componentDidUpdate(prevProps) {
const isNewScene = prevProps.data.username !== this.props.data.username;
let isUpdated = false;
// if (
// this.props.data.data.objects.length !== prevProps.data.data.objects.length
// ) {
// isUpdated = true;
// }
// if (this.props.data.data.body !== prevProps.data.data.body) {
// isUpdated = true;
// }
if (isNewScene || isUpdated) {
this.setStatus(this.props.viewer);
}
}
setStatus = (viewer) => {
let newState = { trustStatus: "untrusted", followStatus: false };
let trust = viewer.trusted.filter((entry) => {
return entry.target_user_id === this.props.data.id;
});
if (trust.length) {
let relation = trust[0];
newState.trustId = relation.id;
if (relation.data.verified) {
newState.trustStatus = "trusted";
} else {
newState.trustStatus = "sent";
}
}
let pendingTrust = viewer.pendingTrusted.filter((entry) => {
return entry.owner_user_id === this.props.data.id;
});
if (pendingTrust.length) {
let relation = pendingTrust[0];
newState.trustId = relation.id;
if (pendingTrust[0].data.verified) {
newState.trustStatus = "trusted";
} else {
newState.trustStatus = "received";
}
}
if (
viewer.subscriptions.filter((entry) => {
return entry.target_user_id === this.props.data.id;
}).length
) {
newState.followStatus = true;
}
this.setState(newState);
};
_handleUpdate = async (e) => {
let response = await this.props.onRehydrate();
if (!response || response.error) {
alert("TODO: error fetching authenticated viewer");
return null;
}
let viewer = response.data;
this.setStatus(viewer);
};
_handleTrust = async () => {
let response;
if (
this.state.trustStatus === "untrusted" ||
this.state.trustStatus === "sent"
) {
response = await Actions.createTrustRelationship({
userId: this.props.data.id,
});
console.log(response);
} else if (this.state.trustStatus === "received") {
response = await Actions.updateTrustRelationship({
userId: this.props.data.id,
});
console.log(response);
} else {
response = await Actions.deleteTrustRelationship({
id: this.state.trustId,
});
console.log(response);
}
await this._handleUpdate();
};
_handleFollow = async () => {
let response = await Actions.createSubscription({
userId: this.props.data.id,
});
console.log(response);
await this._handleUpdate();
};
render() {
let buttons = (
<div>
<ButtonPrimary
style={
this.state.followStatus
? BUTTON_SECONDARY_STYLE
: BUTTON_PRIMARY_STYLE
}
onClick={this._handleFollow}
>
{this.state.followStatus ? "Unfollow" : "Follow"}
</ButtonPrimary>
<ButtonPrimary
style={
this.state.trustStatus === "untrusted" ||
this.state.trustStatus === "received"
? BUTTON_PRIMARY_STYLE
: BUTTON_SECONDARY_STYLE
}
onClick={this._handleTrust}
>
{STATUS_BUTTON_MAP[this.state.trustStatus]}
</ButtonPrimary>
{this.state.isTrusted ? (
<ButtonPrimary style={BUTTON_STYLE} onClick={this._handleSendMoney}>
Send Money
</ButtonPrimary>
) : null}
</div>
);
return (
<ScenePage style={{ padding: `88px 24px 128px 24px` }}>
<Profile
onAction={this.props.onAction}
creator={this.props.data}
sceneId={this.props.sceneId}
buttons={
this.props.viewer.username === this.props.data.username
? null
: buttons
}
/>
</ScenePage>
);
}
}

View File

@ -12,6 +12,14 @@ import Slate, { generateLayout } from "~/components/core/Slate";
import SlateMediaObject from "~/components/core/SlateMediaObject";
import CircleButtonGray from "~/components/core/CircleButtonGray";
const STYLES_USERNAME = css`
cursor: pointer;
:hover {
color: ${Constants.system.brand};
}
`;
const moveIndex = (set, fromIndex, toIndex) => {
const element = set[fromIndex];
set.splice(fromIndex, 1);
@ -22,15 +30,17 @@ const moveIndex = (set, fromIndex, toIndex) => {
export default class SceneSlate extends React.Component {
state = {
name: this.props.current.data.name,
slatename: this.props.current.slatename,
public: this.props.current.data.public,
objects: this.props.current.data.objects,
body: this.props.current.data.body,
layouts: this.props.current.data.layouts
? this.props.current.data.layouts
: { lg: generateLayout(this.props.current.data.objects) },
name: this.props.data.data.name,
username: this.props.data.owner ? this.props.data.owner.username : null,
slatename: this.props.data.slatename,
public: this.props.data.data.public,
objects: this.props.data.data.objects,
body: this.props.data.data.body,
layouts: this.props.data.data.layouts
? this.props.data.data.layouts
: { lg: generateLayout(this.props.data.data.objects) },
loading: false,
editing: this.props.data.data.ownerId === this.props.viewer.id,
};
componentDidMount() {
@ -39,38 +49,41 @@ export default class SceneSlate extends React.Component {
componentDidUpdate(prevProps) {
const isNewSlateScene =
prevProps.current.slatename !== this.props.current.slatename;
prevProps.data.slatename !== this.props.data.slatename;
let isUpdated = false;
if (
this.props.current.data.objects.length !==
prevProps.current.data.objects.length
this.props.data.data.objects.length !== prevProps.data.data.objects.length
) {
isUpdated = true;
}
if (this.props.current.data.body !== prevProps.current.data.body) {
if (this.props.data.data.body !== prevProps.data.data.body) {
isUpdated = true;
}
if (isNewSlateScene || isUpdated) {
let layouts = this.props.current.data.layouts;
let layouts = this.props.data.data.layouts;
if (!layouts) {
layouts = { lg: generateLayout(this.props.current.data.objects) };
layouts = { lg: generateLayout(this.props.data.data.objects) };
}
this.setState({
slatename: this.props.current.slatename,
public: this.props.current.data.public,
objects: this.props.current.data.objects,
body: this.props.current.data.body,
name: this.props.current.data.name,
username: this.props.data.owner ? this.props.data.owner.username : null,
slatename: this.props.data.slatename,
public: this.props.data.data.public,
objects: this.props.data.data.objects,
body: this.props.data.data.body,
name: this.props.data.data.name,
layouts: layouts,
loading: false,
editing: this.props.viewer.slates
.map((slate) => slate.id)
.includes(this.props.data.slateId),
});
this._handleUpdateCarousel({
objects: this.props.current.data.objects,
objects: this.props.data.data.objects,
});
}
}
@ -93,7 +106,7 @@ export default class SceneSlate extends React.Component {
this.setState({ loading: true });
const response = await Actions.updateSlate({
id: this.props.current.slateId,
id: this.props.data.id,
data: {
objects: objects ? objects : this.state.objects,
layouts: layouts ? layouts : this.state.layouts,
@ -164,6 +177,7 @@ export default class SceneSlate extends React.Component {
id: data.id,
cid,
data,
editing: this.state.editing,
component: (
<SlateMediaObject key={each.id} useImageFallback data={data} />
),
@ -199,7 +213,7 @@ export default class SceneSlate extends React.Component {
}
const response = await Actions.updateSlate({
id: this.props.current.slateId,
id: this.props.data.slateId,
data: {
objects,
layouts,
@ -245,7 +259,7 @@ export default class SceneSlate extends React.Component {
return this.props.onAction({
type: "SIDEBAR",
value: "SIDEBAR_ADD_FILE_TO_BUCKET",
data: this.props.current,
data: this.props.data,
});
};
@ -253,41 +267,70 @@ export default class SceneSlate extends React.Component {
return this.props.onAction({
type: "SIDEBAR",
value: "SIDEBAR_SINGLE_SLATE_SETTINGS",
data: this.props.current,
data: this.props.data,
});
};
render() {
const { slatename, objects, body = "A slate.", name } = this.state;
const {
username,
slatename,
objects,
body = "A slate.",
name,
} = this.state;
return (
<ScenePage style={{ padding: `88px 24px 128px 24px` }}>
<ScenePageHeader
style={{ padding: `0 24px 0 24px` }}
title={name}
title={
username ? (
<span>
<span
onClick={() =>
this.props.onAction({
type: "NAVIGATE",
value: this.props.sceneId,
scene: "PROFILE",
data: this.props.data.owner,
})
}
css={STYLES_USERNAME}
>
{username}
</span>{" "}
/ {slatename}
</span>
) : (
slatename
)
}
actions={
<React.Fragment>
<CircleButtonGray
onMouseUp={this._handleAdd}
onTouchEnd={this._handleAdd}
style={{ marginLeft: 12, marginRight: 12 }}
>
<SVG.Plus height="16px" />
</CircleButtonGray>
<CircleButtonGray
onMouseUp={this._handleShowSettings}
onTouchEnd={this._handleShowSettings}
>
<SVG.Settings height="16px" />
</CircleButtonGray>
</React.Fragment>
this.state.editing ? (
<React.Fragment>
<CircleButtonGray
onMouseUp={this._handleAdd}
onTouchEnd={this._handleAdd}
style={{ marginLeft: 12, marginRight: 12 }}
>
<SVG.Plus height="16px" />
</CircleButtonGray>
<CircleButtonGray
onMouseUp={this._handleShowSettings}
onTouchEnd={this._handleShowSettings}
>
<SVG.Settings height="16px" />
</CircleButtonGray>
</React.Fragment>
) : null
}
>
{body}
</ScenePageHeader>
<Slate
key={slatename}
editing
editing={this.state.editing}
items={objects}
layouts={this.state.layouts}
onLayoutChange={this._handleChangeLayout}

View File

@ -7,72 +7,98 @@ import { css } from "@emotion/react";
import ScenePage from "~/components/core/ScenePage";
import ScenePageHeader from "~/components/core/ScenePageHeader";
import Section from "~/components/core/Section";
import SlatePreviewBlock from "~/components/core/SlatePreviewBlock";
const STYLES_NUMBER = css`
font-family: ${Constants.font.semiBold};
font-weight: 400;
`;
const STYLES_ACTIONS = css`
z-index: ${Constants.zindex.navigation};
bottom: 16px;
right: 8px;
position: absolute;
flex-direction: column;
display: flex;
`;
const STYLES_ACTION_BUTTON = css`
font-family: ${Constants.font.code};
font-size: 10px;
text-transform: uppercase;
user-select: none;
height: 32px;
padding: 0 16px 0 16px;
border-radius: 32px;
display: inline-flex;
align-items: center;
justify-content: center;
z-index: ${Constants.zindex.modal};
background: ${Constants.system.pitchBlack};
transition: 200ms ease all;
color: ${Constants.system.white};
cursor: pointer;
margin: auto;
margin: 4px 16px 4px 16px;
flex-shrink: 0;
text-decoration: none;
:hover {
background-color: ${Constants.system.black};
}
`;
// TODO(jim): Slates design.
export default class SceneSlates extends React.Component {
render() {
// TODO(jim): Refactor later.
const slates = {
columns: [
{
key: "name",
name: "Slate Name",
width: "100%",
type: "SLATE_LINK",
},
{ key: "url", name: "URL", width: "268px", type: "NEW_WINDOW" },
{ key: "id", id: "id", name: "Slate ID", width: "296px" },
{
key: "objects",
name: "Objects",
},
{
key: "public",
name: "Public",
type: "SLATE_PUBLIC_TEXT_TAG",
width: "188px",
},
],
rows: this.props.viewer.slates.map((each) => {
return {
...each,
name: each.data.name,
url: `https://slate.host/${this.props.viewer.username}/${
each.slatename
}`,
public: each.data.public,
objects: <span css={STYLES_NUMBER}>{each.data.objects.length}</span>,
};
}),
};
const slates = this.props.viewer.slates.map((each) => {
return {
...each,
url: `https://slate.host/${this.props.viewer.username}/${each.slatename}`,
public: each.data.public,
objects: <span css={STYLES_NUMBER}>{each.data.objects.length}</span>,
};
});
// TODO(jim): Refactor later.
const slateButtons = [
{ name: "Create slate", type: "SIDEBAR", value: "SIDEBAR_CREATE_SLATE" },
];
console.log(this.props);
return (
<ScenePage>
<ScenePageHeader title="Slates [WIP]">
<ScenePageHeader title="Slates">
This scene is currently a work in progress.
</ScenePageHeader>
<Section
title="Slates"
buttons={slateButtons}
onAction={this.props.onAction}
>
<System.Table
data={slates}
name="slate"
onAction={this.props.onAction}
onNavigateTo={this.props.onNavigateTo}
/>
</Section>
{this.props.data.children.map((slate) => (
<div
key={slate.id}
onClick={() =>
this.props.onAction({
type: "NAVIGATE",
value: slate.id,
data: slate,
})
}
>
<SlatePreviewBlock slate={slate} />
</div>
))}
<div css={STYLES_ACTIONS}>
<span
css={STYLES_ACTION_BUTTON}
onClick={() =>
this.props.onAction({
name: "Create slate",
type: "SIDEBAR",
value: "SIDEBAR_CREATE_SLATE",
})
}
>
Create slate
</span>
</div>
</ScenePage>
);
}