-
-
+
{navbarStart}
{!isSignedIn && (
-
- Home
-
-
- About
-
-
- Testimony
-
-
- Benefits
-
-
- News
+
+ Login
@@ -108,29 +62,7 @@ export function NavigationBar({
)}
- {isSignedIn && (
-
-
- 📦 Upload
-
-
- 🔍 Search
-
-
- 🗄 Uploads
-
- {
- event.preventDefault();
- backend.logout();
- }}
- >
- 🔒 log out
-
-
- )}
+ {isSignedIn && getNavbarEnd(path, backend)}
);
diff --git a/web/src/components/NavigationBar/helpers/getNavbarEnd.tsx b/web/src/components/NavigationBar/helpers/getNavbarEnd.tsx
new file mode 100644
index 000000000..0811a240c
--- /dev/null
+++ b/web/src/components/NavigationBar/helpers/getNavbarEnd.tsx
@@ -0,0 +1,22 @@
+import Backend from '../../../lib/Backend';
+import NavbarItem from '../NavbarItem';
+
+export default function getNavbarEnd(path: string, backend: Backend) {
+ return (
+
+
+ 🔍 Search
+
+ {
+ event.preventDefault();
+ backend.logout();
+ }}
+ >
+ 🔒 log out
+
+
+ );
+}
diff --git a/web/src/components/NavigationBar/helpers/getNavbarStartNewUser.tsx b/web/src/components/NavigationBar/helpers/getNavbarStartNewUser.tsx
new file mode 100644
index 000000000..b613b128e
--- /dev/null
+++ b/web/src/components/NavigationBar/helpers/getNavbarStartNewUser.tsx
@@ -0,0 +1,23 @@
+import NavbarItem from '../NavbarItem';
+
+export default function getNavbarStartNewUser(hash: string, path: string) {
+ return (
+ <>
+
+ Home
+
+
+ About
+
+
+ Testimony
+
+
+ Benefits
+
+
+ News
+
+ >
+ );
+}
diff --git a/web/src/components/NavigationBar/helpers/getNavbarStartRegularUser.tsx b/web/src/components/NavigationBar/helpers/getNavbarStartRegularUser.tsx
new file mode 100644
index 000000000..d3e267fce
--- /dev/null
+++ b/web/src/components/NavigationBar/helpers/getNavbarStartRegularUser.tsx
@@ -0,0 +1,20 @@
+import NavbarItem from '../NavbarItem';
+
+export default function getNavbarStartRegularUser(path: string) {
+ /**
+ * Coming soon
+ * /learn
+ * /import
+ */
+ return (
+ <>
+
+ 📦 Upload
+
+
+ 🗂 My Uploads
+
+
👩🏼🎨 Templates
+ >
+ );
+}
diff --git a/web/src/components/icons/DotsHorizontal.tsx b/web/src/components/icons/DotsHorizontal.tsx
new file mode 100644
index 000000000..b68bd5c66
--- /dev/null
+++ b/web/src/components/icons/DotsHorizontal.tsx
@@ -0,0 +1,18 @@
+interface IconProps {
+ width: number;
+ height: number;
+}
+
+export default function DotsHorizontal(props: IconProps) {
+ const { width, height } = props;
+ return (
+
+
+
+ );
+}
diff --git a/web/src/components/icons/Heart.tsx b/web/src/components/icons/Heart.tsx
new file mode 100644
index 000000000..77ae110af
--- /dev/null
+++ b/web/src/components/icons/Heart.tsx
@@ -0,0 +1,24 @@
+interface IconProps {
+ width: number;
+ height: number;
+}
+
+export default function Heart(props: IconProps) {
+ const { width, height } = props;
+ return (
+
+
+
+ );
+}
diff --git a/web/src/components/icons/SolidHeart.tsx b/web/src/components/icons/SolidHeart.tsx
new file mode 100644
index 000000000..122c0d99d
--- /dev/null
+++ b/web/src/components/icons/SolidHeart.tsx
@@ -0,0 +1,22 @@
+interface IconProps {
+ width: number;
+ height: number;
+}
+
+export default function SolidHeart(props: IconProps) {
+ const { width, height } = props;
+ return (
+
+
+
+ );
+}
diff --git a/web/src/components/input/Switch.tsx b/web/src/components/input/Switch.tsx
index 9630600ef..1dd839c71 100644
--- a/web/src/components/input/Switch.tsx
+++ b/web/src/components/input/Switch.tsx
@@ -26,6 +26,7 @@ function Switch({
name={id}
className="switch is-rounded is-info"
checked={checked}
+ onChange={() => onSwitched()}
/>
{title}
diff --git a/web/src/components/modals/SettingsModal.tsx b/web/src/components/modals/SettingsModal.tsx
index 67ac45995..386fbfacd 100644
--- a/web/src/components/modals/SettingsModal.tsx
+++ b/web/src/components/modals/SettingsModal.tsx
@@ -47,12 +47,11 @@ interface Props {
pageId?: string;
isActive: boolean;
onClickClose: React.MouseEventHandler;
- setError: (error: string) => void;
}
const backend = new Backend();
function SettingsModal({
- pageTitle, pageId, isActive, onClickClose, setError,
+ pageTitle, pageId, isActive, onClickClose,
}: Props) {
const [settings, setSettings] = useState(null);
const [loading, setLoading] = useState(!!pageId);
@@ -105,7 +104,7 @@ function SettingsModal({
}
setLoading(false);
})
- .catch((error) => setError(error.data.response.message));
+ .catch((error) => { store.error = error; });
}
}, [pageId]);
@@ -149,7 +148,9 @@ function SettingsModal({
.then(() => {
onClickClose(event);
})
- .catch((error) => setError(error.data.response.message));
+ .catch((error) => {
+ store.error = error;
+ });
};
return (
diff --git a/web/src/components/styled.tsx b/web/src/components/styled.tsx
index 1081dccaf..1ab89690c 100644
--- a/web/src/components/styled.tsx
+++ b/web/src/components/styled.tsx
@@ -15,5 +15,9 @@ export const Main = styled.main`
`;
export const HomeContainer = styled(Container)`
-padding: 0;
+ padding: 0;
+`;
+
+export const PageContainer = styled.div`
+ margin: 0 auto;
`;
diff --git a/web/src/lib/Backend.ts b/web/src/lib/Backend.ts
index 4ca6c2ac8..2a3df32c5 100644
--- a/web/src/lib/Backend.ts
+++ b/web/src/lib/Backend.ts
@@ -6,6 +6,7 @@ import UserJob from './interfaces/UserJob';
import getObjectTitle from './notion/getObjectTitle';
import getObjectIcon from './notion/getObjectIcon';
+import FavoriteObject from './interfaces/FavoriteObject';
class Backend {
baseURL: string;
@@ -18,6 +19,7 @@ class Backend {
async logout() {
localStorage.clear();
+ sessionStorage.clear();
const endpoint = `${this.baseURL}users/logout`;
await axios.get(endpoint, { withCredentials: true });
window.location.href = '/';
@@ -90,6 +92,7 @@ class Backend {
'You are making too many requests. Please wait a few seconds before searching.',
);
}
+ const favorites = await this.getFavorites();
const isObjectId = query.replace(/-/g, '').length === 32;
let data;
@@ -123,12 +126,16 @@ class Backend {
icon: getObjectIcon(p),
url: p.url as string,
id: p.id,
+ isFavorite: favorites.some((f) => f.id === p.id),
}));
}
return [];
}
- async getPage(pageId: string): Promise
{
+ async getPage(
+ pageId: string,
+ isFavorite: boolean = false,
+ ): Promise {
try {
const response = await axios.get(`${this.baseURL}notion/page/${pageId}`, {
withCredentials: true,
@@ -140,13 +147,17 @@ class Backend {
url: response.data.url as string,
id: response.data.id,
data: response.data,
+ isFavorite,
};
} catch (error) {
return null;
}
}
- async getDatabase(id: string): Promise {
+ async getDatabase(
+ id: string,
+ isFavorite: boolean = false,
+ ): Promise {
try {
const response = await axios.get(`${this.baseURL}notion/database/${id}`, {
withCredentials: true,
@@ -158,6 +169,7 @@ class Backend {
url: response.data.url as string,
id: response.data.id,
data: response.data,
+ isFavorite,
};
} catch (error) {
return null;
@@ -218,6 +230,36 @@ class Backend {
});
return response.data.patreon;
}
+
+ async addFavorite(id: string, type: string): Promise {
+ return axios.post(
+ `${this.baseURL}favorite/create`,
+ { id, type },
+ {
+ withCredentials: true,
+ },
+ );
+ }
+
+ async deleteFavorite(id: string): Promise {
+ return axios.post(
+ `${this.baseURL}favorite/remove`,
+ { id },
+ {
+ withCredentials: true,
+ },
+ );
+ }
+
+ async getFavorites(): Promise {
+ const response = await axios.get(`${this.baseURL}favorite`, {
+ withCredentials: true,
+ });
+ const getObject = (f: FavoriteObject) => (f.type === 'page'
+ ? this.getPage(f.object_id, true)
+ : this.getDatabase(f.object_id, true));
+ return Promise.all(response.data.map(async (f) => getObject(f)));
+ }
}
export default Backend;
diff --git a/web/src/lib/interfaces/FavoriteObject.ts b/web/src/lib/interfaces/FavoriteObject.ts
new file mode 100644
index 000000000..f27d4b505
--- /dev/null
+++ b/web/src/lib/interfaces/FavoriteObject.ts
@@ -0,0 +1,5 @@
+export default interface FavoriteObject {
+ object_id: string;
+ owner: string;
+ type: string;
+}
\ No newline at end of file
diff --git a/web/src/lib/interfaces/NotionObject.ts b/web/src/lib/interfaces/NotionObject.ts
index 6d77a2590..eafac8d9f 100644
--- a/web/src/lib/interfaces/NotionObject.ts
+++ b/web/src/lib/interfaces/NotionObject.ts
@@ -5,6 +5,7 @@ interface NotionObject {
icon?: string;
id: string;
data?: any;
+ isFavorite?: boolean;
}
export default NotionObject;
diff --git a/web/src/pages/Import/ImportPage.tsx b/web/src/pages/Import/ImportPage.tsx
new file mode 100644
index 000000000..c5d2c6a3e
--- /dev/null
+++ b/web/src/pages/Import/ImportPage.tsx
@@ -0,0 +1,21 @@
+import { PageContainer } from '../../components/styled';
+
+export default function ImportPage() {
+ return (
+
+
+
Import
+
+
Our main focus right now is to get better support for Notion.
+
More formats are on the roadmap like:
+
+ APKG → Notion
+ CSV → Anki
+ TSV → Anki
+ PDF → Anki
+ YouTube → Anki
+
+
+
+ );
+}
diff --git a/web/src/pages/Import/index.tsx b/web/src/pages/Import/index.tsx
new file mode 100644
index 000000000..5d194d550
--- /dev/null
+++ b/web/src/pages/Import/index.tsx
@@ -0,0 +1,3 @@
+import ImportPage from './ImportPage';
+
+export default ImportPage;
diff --git a/web/src/pages/Learn/index.tsx b/web/src/pages/Learn/index.tsx
index 17a549ef8..96e628730 100644
--- a/web/src/pages/Learn/index.tsx
+++ b/web/src/pages/Learn/index.tsx
@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react';
-import { Container } from '../../components/styled';
+import { Container, PageContainer } from '../../components/styled';
import Backend from '../../lib/Backend';
import Wrapper from './Wrapper';
@@ -62,46 +62,49 @@ function LearnPage({ setError }: Props) {
return insert loading screen.
;
}
return (
-
-
- {page && (
-
-
-
- )}
-
- {block && (
- <>
-
{block.id}
-
{JSON.stringify(block, null, 4)}
-
-
{JSON.stringify(grandChild, null, 2)}
- >
+
+ Learn
+
+
+ {page && (
+
+
+
)}
-
-
- {location + 1}
- {' '}
- /
- {children.length}
-
-
-
-
+
+ {block && (
+ <>
+
{block.id}
+
{JSON.stringify(block, null, 4)}
+
+
{JSON.stringify(grandChild, null, 2)}
+ >
+ )}
+
+
+ {location + 1}
+ {' '}
+ /
+ {children.length}
+
+
+
+
+
);
}
diff --git a/web/src/pages/Uploads/components/ActiveJobs.tsx b/web/src/pages/MyUploads/components/ActiveJobs.tsx
similarity index 100%
rename from web/src/pages/Uploads/components/ActiveJobs.tsx
rename to web/src/pages/MyUploads/components/ActiveJobs.tsx
diff --git a/web/src/pages/Uploads/components/UploadObjectEntry.tsx b/web/src/pages/MyUploads/components/UploadObjectEntry.tsx
similarity index 100%
rename from web/src/pages/Uploads/components/UploadObjectEntry.tsx
rename to web/src/pages/MyUploads/components/UploadObjectEntry.tsx
diff --git a/web/src/pages/Uploads/hooks/useActiveJobs.tsx b/web/src/pages/MyUploads/hooks/useActiveJobs.tsx
similarity index 100%
rename from web/src/pages/Uploads/hooks/useActiveJobs.tsx
rename to web/src/pages/MyUploads/hooks/useActiveJobs.tsx
diff --git a/web/src/pages/Uploads/hooks/usePatreon.tsx b/web/src/pages/MyUploads/hooks/usePatreon.tsx
similarity index 100%
rename from web/src/pages/Uploads/hooks/usePatreon.tsx
rename to web/src/pages/MyUploads/hooks/usePatreon.tsx
diff --git a/web/src/pages/Uploads/hooks/useQuota.tsx b/web/src/pages/MyUploads/hooks/useQuota.tsx
similarity index 100%
rename from web/src/pages/Uploads/hooks/useQuota.tsx
rename to web/src/pages/MyUploads/hooks/useQuota.tsx
diff --git a/web/src/pages/Uploads/hooks/useUploads.tsx b/web/src/pages/MyUploads/hooks/useUploads.tsx
similarity index 100%
rename from web/src/pages/Uploads/hooks/useUploads.tsx
rename to web/src/pages/MyUploads/hooks/useUploads.tsx
diff --git a/web/src/pages/MyUploads/index.tsx b/web/src/pages/MyUploads/index.tsx
new file mode 100644
index 000000000..ae9bb62ab
--- /dev/null
+++ b/web/src/pages/MyUploads/index.tsx
@@ -0,0 +1,133 @@
+import BecomeAPatron from '../../components/BecomeAPatron';
+import UploadObjectEntry from './components/UploadObjectEntry';
+import LoadingScreen from '../../components/LoadingScreen';
+import Backend from '../../lib/Backend';
+import ActiveJobs from './components/ActiveJobs';
+
+import useUploads from './hooks/useUploads';
+import usePatreon from './hooks/usePatreon';
+import useQuota from './hooks/useQuota';
+import useActiveJobs from './hooks/useActiveJobs';
+import { Container, PageContainer } from '../../components/styled';
+
+const backend = new Backend();
+
+interface MyUploadsPageProps {
+ setError: (error: string) => void;
+}
+
+function MyUploadsPage({ setError }: MyUploadsPageProps) {
+ const [
+ loading, uploads, deleteUpload, deleteAllUploads,
+ isDeletingAll,
+ ] = useUploads(backend, setError);
+ const [activeJobs, deleteJob] = useActiveJobs(backend, setError);
+ const [isPatreon] = usePatreon(backend, setError);
+ const [quota] = useQuota(uploads);
+
+ if (loading) return ;
+
+ return (
+
+
+ {activeJobs.length > 0 && (
+ deleteJob(id)} />
+ )}
+ My Uploads
+ {uploads.length === 0 && !loading && (
+
+ You have no uploads! Make some from the
+ {' '}
+
+ search
+
+ {' '}
+ page.
+
+ )}
+ {uploads.length > 0 && (
+ <>
+ {uploads
+ && uploads.map((u) => (
+ deleteUpload(u.key)}
+ />
+ ))}
+
+ {!isPatreon && (
+
+
+
+ You have used
+ {' '}
+ {quota.toFixed(2)}
+ {' '}
+ MB
+ {!isPatreon && ' of your quota (21MB)'}
+ .
+
+ {
+ deleteAllUploads();
+ }}
+ >
+ Delete All
+
+
+
16 ? 'is-danger' : 'is-info'}`}
+ value={quota}
+ max={21}
+ >
+ 15%
+
+
+
+
+
Imposed limitations
+
+ We have set quota limits on non-patrons to avoid increasing
+ server load. The limitations are:
+
+
+
+ You can only make conversions totalling 21MB but this is
+ not permanent. You can for example delete previous uploads
+ to reclaim your space when using it all up.
+
+
+ You can only convert at most 21 subpages (applies to
+ database entries as well) per conversion job.
+
+
+ Max 1 conversion job but you can start new ones as soon as
+ the last one is completed.
+
+ You can only load 21 blocks total per page.
+
+
+ If you want the limits removed you can do so by becoming a
+ patron and they will removed for your account.
+
+
+
+
+
+ )}
+ >
+ )}
+
+
+ );
+}
+
+export default MyUploadsPage;
diff --git a/web/src/pages/Search/components/ConnectNotion.tsx b/web/src/pages/Search/components/ConnectNotion.tsx
new file mode 100644
index 000000000..ed7a9c4f1
--- /dev/null
+++ b/web/src/pages/Search/components/ConnectNotion.tsx
@@ -0,0 +1,25 @@
+interface Props {
+ connectionLink: string;
+}
+
+export default function ConnectNotion({ connectionLink }: Props) {
+ return (
+
+ );
+}
diff --git a/web/src/pages/Search/components/DefineRules.tsx b/web/src/pages/Search/components/DefineRules.tsx
index 925c5a30a..0112ef7ab 100644
--- a/web/src/pages/Search/components/DefineRules.tsx
+++ b/web/src/pages/Search/components/DefineRules.tsx
@@ -1,16 +1,22 @@
-import { useEffect, useState } from 'react';
+import {
+ Dispatch, SetStateAction, useContext, useEffect, useState,
+} from 'react';
import Switch from '../../../components/input/Switch';
import SettingsModal from '../../../components/modals/SettingsModal';
import TemplateSelect from '../../../components/TemplateSelect';
import Backend from '../../../lib/Backend';
+import NotionObject from '../../../lib/interfaces/NotionObject';
+import StoreContext from '../../../store/StoreContext';
import FlashcardType from './FlashcardType';
interface Props {
+ type: string;
id: string;
setDone: () => void;
parent: string;
- setError: (error: string) => void;
+ isFavorite: boolean;
+ setFavorites: Dispatch>;
}
const flashCardOptions = [
@@ -23,9 +29,10 @@ const flashCardOptions = [
const tagOptions = ['heading', 'strikethrough'];
const backend = new Backend();
-function DefineRules({
- id, setDone, parent, setError,
-}: Props) {
+function DefineRules(props: Props) {
+ const {
+ type, id, setDone, parent, isFavorite, setFavorites,
+ } = props;
const [rules, setRules] = useState({
flashcard_is: ['toggle'],
sub_deck_is: 'child_page',
@@ -40,6 +47,9 @@ function DefineRules({
const [tags, setTags] = useState(rules.tags_is);
const [sendEmail, setSendEmail] = useState(rules.email_notification);
const [more, setMore] = useState(false);
+ const [favorite, setFavorite] = useState(isFavorite);
+
+ const store = useContext(StoreContext);
useEffect(() => {
backend
@@ -54,7 +64,7 @@ function DefineRules({
setIsloading(false);
})
.catch((error) => {
- setError(error.response.data.message);
+ store.error = error;
});
}, [id]);
@@ -75,9 +85,8 @@ function DefineRules({
);
setDone();
} catch (error) {
- setError(error.response.data.message);
+ store.error = error;
}
- setIsloading(false);
};
const onSelectedFlashcardTypes = (fco: string) => {
@@ -90,6 +99,17 @@ function DefineRules({
setFlashcard((prevState) => Array.from(new Set([...prevState, ...rules.flashcard_is])));
};
+ const toggleFavorite = async () => {
+ if (favorite) {
+ await backend.deleteFavorite(id);
+ } else {
+ await backend.addFavorite(id, type);
+ }
+ const favorites = await backend.getFavorites();
+ setFavorites(favorites);
+ setFavorite(!favorite);
+ };
+
return (
@@ -127,7 +147,6 @@ function DefineRules({
onClickClose={() => {
setMore(false);
}}
- setError={setError}
/>
)}
@@ -136,6 +155,7 @@ function DefineRules({
{flashCardOptions.map((fco) => (
onSelectedFlashcardTypes(name)}
@@ -165,6 +185,13 @@ function DefineRules({
setSendEmail(rules.email_notification);
}}
/>
+
{
+ event.preventDefault();
+ saveRules(event);
+ }}
>
Save
diff --git a/web/src/pages/Search/components/Favorites.tsx b/web/src/pages/Search/components/Favorites.tsx
new file mode 100644
index 000000000..db1b37400
--- /dev/null
+++ b/web/src/pages/Search/components/Favorites.tsx
@@ -0,0 +1,24 @@
+import { Dispatch, SetStateAction } from 'react';
+import NotionObject from '../../../lib/interfaces/NotionObject';
+import ListSearchResults from './ListSearchResults';
+
+interface FavoritesProps {
+ favorites: NotionObject[];
+ setFavorites: Dispatch>;
+}
+
+export default function Favorites(props: FavoritesProps) {
+ const { favorites, setFavorites } = props;
+ if (favorites.length < 1) return null;
+
+ return (
+
+
Favorites
+
+
+ );
+}
diff --git a/web/src/pages/Search/components/ListSearchResults.tsx b/web/src/pages/Search/components/ListSearchResults.tsx
new file mode 100644
index 000000000..f0c23c713
--- /dev/null
+++ b/web/src/pages/Search/components/ListSearchResults.tsx
@@ -0,0 +1,44 @@
+import { Dispatch, SetStateAction } from 'react';
+import NotionObject from '../../../lib/interfaces/NotionObject';
+import SearchObjectEntry from './SearchObjectEntry';
+
+interface ListSearchResultsProps {
+ results: NotionObject[];
+ setFavorites: Dispatch>;
+ handleEmpty?: boolean;
+}
+
+export default function ListSearchResults(
+ props: ListSearchResultsProps,
+): JSX.Element {
+ const { results, handleEmpty, setFavorites } = props;
+ const isEmpty = results.length < 1;
+
+ if (isEmpty && handleEmpty) {
+ return (
+
+ No search results, try typing something above 👌🏾
+
+ );
+ }
+ return (
+ <>
+ {results.map((p) => (
+
+ ))}
+ >
+ );
+}
+
+ListSearchResults.defaultProps = {
+ handleEmpty: true,
+};
diff --git a/web/src/pages/Search/components/ObjectType.tsx b/web/src/pages/Search/components/ObjectType.tsx
new file mode 100644
index 000000000..924a706cf
--- /dev/null
+++ b/web/src/pages/Search/components/ObjectType.tsx
@@ -0,0 +1,15 @@
+interface ObjecTypeProps {
+ type: string;
+}
+
+export default function ObjectType(props: ObjecTypeProps) {
+ const { type } = props;
+ return (
+
+ );
+}
diff --git a/web/src/pages/Search/components/SearchBar.tsx b/web/src/pages/Search/components/SearchBar.tsx
index e3585e991..5029df0c1 100644
--- a/web/src/pages/Search/components/SearchBar.tsx
+++ b/web/src/pages/Search/components/SearchBar.tsx
@@ -1,4 +1,4 @@
-import { SearchContainer, SearchInput } from './styled';
+import { SearchInput } from './styled';
interface SearchBarProps {
onSearchQueryChanged: (query: string) => void;
@@ -8,7 +8,7 @@ interface SearchBarProps {
function SearchBar({ onSearchQueryChanged, onSearchClicked, inProgress }: SearchBarProps) {
return (
-
+
);
}
diff --git a/web/src/pages/Search/components/SearchContainer.tsx b/web/src/pages/Search/components/SearchContainer.tsx
new file mode 100644
index 000000000..38c0a32be
--- /dev/null
+++ b/web/src/pages/Search/components/SearchContainer.tsx
@@ -0,0 +1,40 @@
+import LoadingScreen from '../../../components/LoadingScreen';
+import { PageContainer } from '../../../components/styled';
+import Backend from '../../../lib/Backend';
+
+import { NotionData } from '../helpers/useNotionData';
+import useSearchQuery from '../helpers/useSearchQuery';
+import SearchPresenter from './SearchPresenter';
+import WorkSpaceHeader from './WorkspaceHeader';
+
+interface SearchContentProps {
+ backend: Backend;
+ notionData: NotionData;
+}
+
+export default function SearchContainer(props: SearchContentProps) {
+ const { backend, notionData } = props;
+ const {
+ myPages,
+ inProgress,
+ triggerSearch,
+ errorNotification,
+ isLoading,
+ setSearchQuery,
+ } = useSearchQuery(backend);
+
+ if (isLoading) return ;
+
+ return (
+
+
+
+
+ );
+}
diff --git a/web/src/pages/Search/components/SearchObjectEntry/index.tsx b/web/src/pages/Search/components/SearchObjectEntry/index.tsx
index 9541beee7..767341214 100644
--- a/web/src/pages/Search/components/SearchObjectEntry/index.tsx
+++ b/web/src/pages/Search/components/SearchObjectEntry/index.tsx
@@ -1,38 +1,42 @@
-import { useState } from 'react';
+import {
+ Dispatch, SetStateAction, useContext, useState,
+} from 'react';
import Backend from '../../../../lib/Backend';
import DefineRules from '../DefineRules';
import ObjectActions from '../actions/ObjectActions';
import ObjectAction from '../actions/ObjectAction';
-import { Entry, ObjectIconAction, ObjectMeta } from './styled';
+import { Entry, ObjectMeta } from './styled';
+import ObjectType from '../ObjectType';
+import DotsHorizontal from '../../../../components/icons/DotsHorizontal';
+import StoreContext from '../../../../store/StoreContext';
+import NotionObject from '../../../../lib/interfaces/NotionObject';
const backend = new Backend();
interface Props {
+ isFavorite: boolean;
title: string;
icon: string;
url: string;
id: string;
type: string;
- setError: (error: string) => void;
+ setFavorites: Dispatch>;
}
-function SearchObjectEntry({
- title, icon, url, id, type, setError,
-}: Props) {
+function SearchObjectEntry(props: Props) {
+ const {
+ title, icon, url, id, type, isFavorite, setFavorites,
+ } = props;
const [showSettings, setShowSettings] = useState(false);
+ const store = useContext(StoreContext);
return (
<>
-
+
{icon && (icon.includes('http') || icon.includes('data:image')) ? (
) : (
@@ -52,14 +56,11 @@ function SearchObjectEntry({
window.location.href = '/uploads';
})
.catch((error) => {
- setError(error.response.data.message);
+ store.error = error;
});
}}
/>
-
+
-
+
{showSettings && (
setShowSettings(false)}
- setError={setError}
/>
)}
>
diff --git a/web/src/pages/Search/components/SearchPresenter.tsx b/web/src/pages/Search/components/SearchPresenter.tsx
new file mode 100644
index 000000000..fc88626e9
--- /dev/null
+++ b/web/src/pages/Search/components/SearchPresenter.tsx
@@ -0,0 +1,66 @@
+import { useHistory } from 'react-router-dom';
+import { Dispatch, SetStateAction } from 'react';
+import { EmptyContainer } from './styled';
+import SearchBar from './SearchBar';
+import NotionObject from '../../../lib/interfaces/NotionObject';
+import ListSearchResults from './ListSearchResults';
+import Favorites from './Favorites';
+import useFavorites from '../helpers/useFavorites';
+import Backend from '../../../lib/Backend';
+
+interface SearchPresenterProps {
+ inProgress: boolean;
+ myPages: NotionObject[];
+ setSearchQuery: Dispatch>;
+ triggerSearch: (force: boolean) => void;
+ errorNotification: Error | null;
+}
+
+export default function SearchPresenter(
+ props: SearchPresenterProps,
+) {
+ const history = useHistory();
+ const {
+ inProgress,
+ myPages,
+ setSearchQuery,
+ triggerSearch,
+ errorNotification,
+ } = props;
+
+ const [favorites, setFavorites] = useFavorites(new Backend());
+
+ return (
+ <>
+ {
+ history.push({
+ pathname: '/search',
+ search: `?q=${s}`,
+ });
+ setSearchQuery(s);
+ }}
+ onSearchClicked={() => triggerSearch(false)}
+ />
+
+
+ {(!myPages || myPages.length < 1) && (
+
+ {errorNotification && (
+
+
{errorNotification.message}
+
+ )}
+ {!errorNotification && (
+
+ No search results, try typing something above 👌🏾
+
+ )}
+
+ )}
+
+
+ >
+ );
+}
diff --git a/web/src/pages/Search/components/WorkspaceHeader.tsx b/web/src/pages/Search/components/WorkspaceHeader.tsx
new file mode 100644
index 000000000..51c3718a7
--- /dev/null
+++ b/web/src/pages/Search/components/WorkspaceHeader.tsx
@@ -0,0 +1,45 @@
+import { useState } from 'react';
+
+import { NotionData } from '../helpers/useNotionData';
+
+interface WorkspaceHeaderProps {
+ notionData: NotionData;
+}
+
+export default function WorkSpaceHeader(props: WorkspaceHeaderProps) {
+ const { notionData } = props;
+ const { workSpace, connectionLink } = notionData;
+ const [hovered, setHovered] = useState(false);
+
+ return (
+ setHovered(true)}
+ onMouseLeave={() => setHovered(false)}
+ className="container content mt-2 mb-0 is-flex is-justify-content-center"
+ >
+
+
+
+ {hovered && (
+
+ )}
+
+
+
+ );
+}
diff --git a/web/src/pages/Search/components/styled.tsx b/web/src/pages/Search/components/styled.tsx
index 5d24646bd..125adfb9e 100644
--- a/web/src/pages/Search/components/styled.tsx
+++ b/web/src/pages/Search/components/styled.tsx
@@ -1,15 +1,8 @@
import styled from 'styled-components';
export const SearchInput = styled.input`
-width: 60vw;
-max-width: 640px;
-`;
-
-export const SearchContainer = styled.div`
-position: sticky;
-margin: 0 auto;
-display: flex;
-justify-content: center;
+ width: 60vw;
+ max-width: 640px;
`;
export const EmptyContainer = styled.div`
@@ -18,7 +11,3 @@ export const EmptyContainer = styled.div`
justify-content: center;
height: 50vh;
`;
-
-export const StyledSearchPage = styled.div`
- margin: 0 auto;
-`;
diff --git a/web/src/pages/Search/helpers/useFavorites.tsx b/web/src/pages/Search/helpers/useFavorites.tsx
new file mode 100644
index 000000000..96f57ad46
--- /dev/null
+++ b/web/src/pages/Search/helpers/useFavorites.tsx
@@ -0,0 +1,20 @@
+import {
+ Dispatch, SetStateAction, useEffect, useState,
+} from 'react';
+import Backend from '../../../lib/Backend';
+import NotionObject from '../../../lib/interfaces/NotionObject';
+
+export default function useFavorites(
+ backend: Backend,
+): [
+ favorites: NotionObject[],
+ setFavorites: Dispatch>,
+ ] {
+ const [favorites, setFavorites] = useState([]);
+ useEffect(() => {
+ backend.getFavorites().then((input) => {
+ setFavorites(input);
+ });
+ });
+ return [favorites, setFavorites];
+}
diff --git a/web/src/pages/Search/helpers/useNotionData.tsx b/web/src/pages/Search/helpers/useNotionData.tsx
new file mode 100644
index 000000000..e600f5ebf
--- /dev/null
+++ b/web/src/pages/Search/helpers/useNotionData.tsx
@@ -0,0 +1,43 @@
+import { useEffect, useState } from 'react';
+import Backend from '../../../lib/Backend';
+
+export interface NotionData {
+ loading: boolean;
+ workSpace: any;
+ connected: boolean;
+ connectionLink: string;
+}
+
+export default function useNotionData(backend: Backend): NotionData {
+ const [connectionLink, updateConnectionLink] = useState('');
+ const [connected, updateConnected] = useState(false);
+ const [workSpace, setWorkSpace] = useState(
+ localStorage.getItem('__workspace'),
+ );
+
+ const [loading, setIsLoading] = useState(false);
+
+ useEffect(() => {
+ backend
+ .getNotionConnectionInfo()
+ .then((response) => {
+ const { data } = response;
+ if (data && !data.isConnected) {
+ updateConnectionLink(data.link);
+ updateConnected(data.isConnected);
+ } else {
+ updateConnectionLink(data.link);
+ updateConnected(true);
+ }
+ setWorkSpace(data.workspace);
+ setIsLoading(false);
+ })
+ .catch(() => {
+ window.location.href = '/login#login';
+ });
+ }, []);
+
+ return {
+ loading, workSpace, connected, connectionLink,
+ };
+}
diff --git a/web/src/pages/Search/helpers/useSearchQuery.tsx b/web/src/pages/Search/helpers/useSearchQuery.tsx
new file mode 100644
index 000000000..89bb834cd
--- /dev/null
+++ b/web/src/pages/Search/helpers/useSearchQuery.tsx
@@ -0,0 +1,68 @@
+import {
+ useCallback, useContext, useEffect, useState,
+} from 'react';
+
+import Backend from '../../../lib/Backend';
+import useQuery from '../../../lib/hooks/useQuery';
+import NotionObject from '../../../lib/interfaces/NotionObject';
+import StoreContext from '../../../store/StoreContext';
+
+interface SearchQuery {
+ isLoading: boolean;
+ myPages: NotionObject[];
+ setError: (error: string) => void;
+ inProgress: boolean;
+ triggerSearch: (force: boolean) => void;
+ errorNotification: string;
+ setSearchQuery: (query: string) => void;
+}
+
+export default function useSearchQuery(backend: Backend): SearchQuery {
+ const query = useQuery();
+
+ const [searchQuery, setSearchQuery] = useState(query.get('q') || '');
+ const [myPages, setMyPages] = useState([]);
+ const [inProgress, setInProgress] = useState(false);
+ const [errorNotification, setError] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+
+ const store = useContext(StoreContext);
+
+ const triggerSearch = useCallback(
+ (force) => {
+ if (inProgress) {
+ return;
+ }
+ setError(null);
+ setInProgress(true);
+ backend
+ .search(searchQuery, force)
+ .then((results) => {
+ setMyPages(results);
+ setInProgress(false);
+ setIsLoading(false);
+ })
+ .catch((error) => {
+ setIsLoading(false);
+ setInProgress(false);
+ store.error = error;
+ });
+ },
+ [inProgress, searchQuery],
+ );
+
+ useEffect(() => {
+ setIsLoading(true);
+ triggerSearch(true);
+ }, []);
+
+ return {
+ myPages,
+ setError,
+ inProgress,
+ triggerSearch,
+ errorNotification,
+ isLoading,
+ setSearchQuery,
+ };
+}
diff --git a/web/src/pages/Search/index.tsx b/web/src/pages/Search/index.tsx
index b677476d6..02888d4b8 100644
--- a/web/src/pages/Search/index.tsx
+++ b/web/src/pages/Search/index.tsx
@@ -1,169 +1,24 @@
-import { Route, Switch, useHistory } from 'react-router-dom';
-import { useCallback, useEffect, useState } from 'react';
-
-import Backend from '../../lib/Backend';
-import SearchBar from './components/SearchBar';
import { NavigationBar } from '../../components/NavigationBar/NavigationBar';
-import SearchObjectEntry from './components/SearchObjectEntry';
import LoadingScreen from '../../components/LoadingScreen';
-import useQuery from '../../lib/hooks/useQuery';
-import { EmptyContainer, StyledSearchPage } from './components/styled';
-import { Container } from '../../components/styled';
+import SearchContainer from './components/SearchContainer';
+import useNotionData from './helpers/useNotionData';
+import Backend from '../../lib/Backend';
+import ConnectNotion from './components/ConnectNotion';
const backend = new Backend();
-
-function SearchContent() {
- const query = useQuery();
- const history = useHistory();
-
- const [searchQuery, setSearchQuery] = useState(query.get('q') || '');
- const [myPages, setMyPages] = useState([]);
- const [inProgress, setInProgress] = useState(false);
- const [errorNotification, setError] = useState(null);
- const [isLoading, setIsLoading] = useState(true);
-
- const triggerSearch = useCallback(
- (force) => {
- if (inProgress) {
- return;
- }
- setError(null);
- setInProgress(true);
- backend
- .search(searchQuery, force)
- .then((results) => {
- setMyPages(results);
- setInProgress(false);
- setIsLoading(false);
- })
- .catch((error) => {
- setIsLoading(false);
- setInProgress(false);
- setError(error);
- });
- },
- [inProgress, searchQuery],
- );
-
- useEffect(() => {
- setIsLoading(true);
- triggerSearch(true);
- }, []);
-
- if (isLoading) return ;
-
- return (
-
-
- {
- history.push({
- pathname: '/search',
- search: `?q=${s}`,
- });
- setSearchQuery(s);
- }}
- onSearchClicked={() => triggerSearch(false)}
- />
-
-
-
- {(!myPages || myPages.length < 1) && (
-
- {errorNotification && (
-
-
{errorNotification.message}
-
- )}
- {!errorNotification && (
-
- No search results, try typing something above 👌🏾
-
- )}
-
- )}
- {myPages
- && myPages.length > 0
- && myPages.map((p) => (
-
- ))}
-
-
-
-
-
- );
-}
-
function SearchPage() {
- const [connectionLink, updateConnectionLink] = useState('');
- const [connected, updateConnected] = useState(false);
- const [workSpace, setWorkSpace] = useState(
- localStorage.getItem('__workspace'),
- );
-
- const [loading, setIsLoading] = useState(false);
-
- useEffect(() => {
- backend
- .getNotionConnectionInfo()
- .then((response) => {
- const { data } = response;
- if (data && !data.isConnected) {
- updateConnectionLink(data.link);
- updateConnected(data.isConnected);
- } else {
- updateConnectionLink(data.link);
- updateConnected(true);
- }
- setWorkSpace(data.workspace);
- setIsLoading(false);
- })
- .catch(() => {
- window.location.href = '/login#login';
- });
- }, []);
-
- if (loading) {
+ const notionData = useNotionData(backend);
+ if (notionData.loading) {
return ;
}
+ const { workSpace, connected, connectionLink } = notionData;
+
return (
<>
- {!connected && (
-
- )}
- {connected && (
-
- )}
+ {!connected && }
+ {connected && }
>
);
}
diff --git a/web/src/pages/Settings/SettingsPage.tsx b/web/src/pages/Settings/SettingsPage.tsx
new file mode 100644
index 000000000..b9b623ff5
--- /dev/null
+++ b/web/src/pages/Settings/SettingsPage.tsx
@@ -0,0 +1,24 @@
+import { PageContainer } from '../../components/styled';
+
+export default function SettingsPage() {
+ return (
+
+
+
Settings
+
+ The settings you apply here will be the default for new converts
+ unless you set new rules.
+
+
+ Settings defined on a page or database will be used. When no settings
+ are set the ones here are used.
+
+
+ list all settings
+ add delete button
+ add make this the default button
+
+
+
+ );
+}
diff --git a/web/src/pages/Settings/index.tsx b/web/src/pages/Settings/index.tsx
new file mode 100644
index 000000000..caf16d9aa
--- /dev/null
+++ b/web/src/pages/Settings/index.tsx
@@ -0,0 +1,3 @@
+import SettingsPage from './SettingsPage';
+
+export default SettingsPage;
diff --git a/web/src/pages/Upload/index.tsx b/web/src/pages/Upload/index.tsx
index 8e45ddad8..49b278de6 100644
--- a/web/src/pages/Upload/index.tsx
+++ b/web/src/pages/Upload/index.tsx
@@ -8,8 +8,14 @@ import UploadForm from './components/UploadForm';
import SettingsIcon from '../../components/icons/SettingsIcon';
import SettingsModal from '../../components/modals/SettingsModal';
import {
- FlexColumn, ImportTitle, InfoMessage, Main, SettingsLink, UploadContainer,
+ FlexColumn,
+ ImportTitle,
+ InfoMessage,
+ Main,
+ SettingsLink,
+ UploadContainer,
} from './styled';
+import { PageContainer } from '../../components/styled';
interface Props {
setErrorMessage: (message: string) => void;
@@ -32,55 +38,54 @@ function UploadPage({ setErrorMessage }: Props) {
}, [store]);
return (
-
-
- {isDevelopment ? : null}
-
- Import
- setShowSettings(true)}>
-
-
- Settings
-
-
-
-
+
+
+
);
}
diff --git a/web/src/pages/Uploads/index.tsx b/web/src/pages/Uploads/index.tsx
deleted file mode 100644
index 2a14bc7c5..000000000
--- a/web/src/pages/Uploads/index.tsx
+++ /dev/null
@@ -1,131 +0,0 @@
-import BecomeAPatron from '../../components/BecomeAPatron';
-import UploadObjectEntry from './components/UploadObjectEntry';
-import LoadingScreen from '../../components/LoadingScreen';
-import Backend from '../../lib/Backend';
-import ActiveJobs from './components/ActiveJobs';
-
-import useUploads from './hooks/useUploads';
-import usePatreon from './hooks/usePatreon';
-import useQuota from './hooks/useQuota';
-import useActiveJobs from './hooks/useActiveJobs';
-import { Container } from '../../components/styled';
-
-const backend = new Backend();
-
-interface ListUploadsPageProps {
- setError: (error: string) => void;
-}
-
-function ListUploadsPage({ setError }: ListUploadsPageProps) {
- const [
- loading, uploads, deleteUpload, deleteAllUploads,
- isDeletingAll,
- ] = useUploads(backend, setError);
- const [activeJobs, deleteJob] = useActiveJobs(backend, setError);
- const [isPatreon] = usePatreon(backend, setError);
- const [quota] = useQuota(uploads);
-
- if (loading) return ;
-
- return (
-
- {activeJobs.length > 0 && (
- deleteJob(id)} />
- )}
- Uploads
- {uploads.length === 0 && !loading && (
-
- You have no uploads! Make some from the
- {' '}
-
- search
-
- {' '}
- page.
-
- )}
- {uploads.length > 0 && (
- <>
- {uploads
- && uploads.map((u) => (
- deleteUpload(u.key)}
- />
- ))}
-
- {!isPatreon && (
-
-
-
- You have used
- {' '}
- {quota.toFixed(2)}
- {' '}
- MB
- {!isPatreon && ' of your quota (21MB)'}
- .
-
- {
- deleteAllUploads();
- }}
- >
- Delete All
-
-
-
16 ? 'is-danger' : 'is-info'}`}
- value={quota}
- max={21}
- >
- 15%
-
-
-
-
-
Imposed limitations
-
- We have set quota limits on non-patrons to avoid increasing
- server load. The limitations are:
-
-
-
- You can only make conversions totalling 21MB but this is
- not permanent. You can for example delete previous uploads
- to reclaim your space when using it all up.
-
-
- You can only convert at most 21 subpages (applies to
- database entries as well) per conversion job.
-
-
- Max 1 conversion job but you can start new ones as soon as
- the last one is completed.
-
- You can only load 21 blocks total per page.
-
-
- If you want the limits removed you can do so by becoming a
- patron and they will removed for your account.
-
-
-
-
-
- )}
- >
- )}
-
- );
-}
-
-export default ListUploadsPage;
diff --git a/web/src/store/CardOptionsStore.ts b/web/src/store/CardOptionsStore.ts
index aefc7f7ed..c4ef6d086 100644
--- a/web/src/store/CardOptionsStore.ts
+++ b/web/src/store/CardOptionsStore.ts
@@ -4,6 +4,8 @@ import CardOption from './CardOption';
class CardOptionsStore {
public options: CardOption[];
+ public error: Error | null;
+
constructor() {
this.options = supportedOptions();
}