mirror of
https://github.com/filecoin-project/slate.git
synced 2024-11-27 10:52:41 +03:00
Merge pull request #169 from filecoin-project/@martinalong/profiles
In-client profiles / slates + directory tab
This commit is contained in:
commit
efdc156448
@ -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,
|
||||
},
|
||||
];
|
||||
|
@ -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) {
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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} /> },
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -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}
|
||||
|
78
components/core/Profile.js
Normal file
78
components/core/Profile.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
@ -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`
|
||||
|
@ -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>
|
309
components/core/SearchModal.js
Normal file
309
components/core/SearchModal.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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 ⭢
|
||||
</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 ⭢
|
||||
</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 ⭢
|
||||
</a>
|
||||
<a
|
||||
key="sidebar-media-download-file"
|
||||
css={STYLES_BUTTON}
|
||||
href={Strings.getCIDGatewayURL(this.props.cid)}
|
||||
target="_blank"
|
||||
download={this.props.cid}
|
||||
>
|
||||
Download file ⭢
|
||||
</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 ⭢</span>
|
||||
<span>Delete from slate ⭢</span>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
|
75
components/core/SlatePreviewBlock.js
Normal file
75
components/core/SlatePreviewBlock.js
Normal 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>
|
||||
);
|
||||
}
|
@ -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,
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
@ -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],
|
||||
|
@ -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();
|
||||
};
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
|
@ -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
198
scenes/SceneProfile.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
@ -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}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user