Browse Source

[feat] Add routing for Notion articles

Sergio Mattei 1 year ago
parent
commit
9fda4b116b

+ 5
- 5
capacitor.config.ts View File

1
-import { CapacitorConfig } from '@capacitor/cli';
1
+import { CapacitorConfig } from "@capacitor/cli";
2
 
2
 
3
 const config: CapacitorConfig = {
3
 const config: CapacitorConfig = {
4
-  appId: 'io.ionic.starter',
5
-  appName: 'renacer',
6
-  webDir: 'build',
7
-  bundledWebRuntime: false
4
+  appId: "io.renacer.social",
5
+  appName: "renacer",
6
+  webDir: "build",
7
+  bundledWebRuntime: false,
8
 };
8
 };
9
 
9
 
10
 export default config;
10
 export default config;

+ 6
- 2
ios/App/App.xcodeproj/project.pbxproj View File

347
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
347
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
348
 				CODE_SIGN_STYLE = Automatic;
348
 				CODE_SIGN_STYLE = Automatic;
349
 				CURRENT_PROJECT_VERSION = 1;
349
 				CURRENT_PROJECT_VERSION = 1;
350
+				DEVELOPMENT_TEAM = N9M43N6FY5;
350
 				INFOPLIST_FILE = App/Info.plist;
351
 				INFOPLIST_FILE = App/Info.plist;
352
+				INFOPLIST_KEY_CFBundleDisplayName = "Renacer Social";
351
 				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
353
 				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
352
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
354
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
353
 				MARKETING_VERSION = 1.0;
355
 				MARKETING_VERSION = 1.0;
354
 				OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
356
 				OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
355
-				PRODUCT_BUNDLE_IDENTIFIER = io.ionic.starter;
357
+				PRODUCT_BUNDLE_IDENTIFIER = io.renacer.social;
356
 				PRODUCT_NAME = "$(TARGET_NAME)";
358
 				PRODUCT_NAME = "$(TARGET_NAME)";
357
 				SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
359
 				SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
358
 				SWIFT_VERSION = 5.0;
360
 				SWIFT_VERSION = 5.0;
367
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
369
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
368
 				CODE_SIGN_STYLE = Automatic;
370
 				CODE_SIGN_STYLE = Automatic;
369
 				CURRENT_PROJECT_VERSION = 1;
371
 				CURRENT_PROJECT_VERSION = 1;
372
+				DEVELOPMENT_TEAM = N9M43N6FY5;
370
 				INFOPLIST_FILE = App/Info.plist;
373
 				INFOPLIST_FILE = App/Info.plist;
374
+				INFOPLIST_KEY_CFBundleDisplayName = "Renacer Social";
371
 				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
375
 				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
372
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
376
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
373
 				MARKETING_VERSION = 1.0;
377
 				MARKETING_VERSION = 1.0;
374
-				PRODUCT_BUNDLE_IDENTIFIER = io.ionic.starter;
378
+				PRODUCT_BUNDLE_IDENTIFIER = io.renacer.social;
375
 				PRODUCT_NAME = "$(TARGET_NAME)";
379
 				PRODUCT_NAME = "$(TARGET_NAME)";
376
 				SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
380
 				SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
377
 				SWIFT_VERSION = 5.0;
381
 				SWIFT_VERSION = 5.0;

+ 1
- 1
ios/App/App/Info.plist View File

5
 	<key>CFBundleDevelopmentRegion</key>
5
 	<key>CFBundleDevelopmentRegion</key>
6
 	<string>en</string>
6
 	<string>en</string>
7
 	<key>CFBundleDisplayName</key>
7
 	<key>CFBundleDisplayName</key>
8
-        <string>renacer</string>
8
+	<string>renacer</string>
9
 	<key>CFBundleExecutable</key>
9
 	<key>CFBundleExecutable</key>
10
 	<string>$(EXECUTABLE_NAME)</string>
10
 	<string>$(EXECUTABLE_NAME)</string>
11
 	<key>CFBundleIdentifier</key>
11
 	<key>CFBundleIdentifier</key>

+ 1047
- 0
package-lock.json
File diff suppressed because it is too large
View File


+ 4
- 0
package.json View File

23
     "@types/react-router-dom": "^5.1.7",
23
     "@types/react-router-dom": "^5.1.7",
24
     "history": "^4.9.0",
24
     "history": "^4.9.0",
25
     "ionicons": "^6.0.3",
25
     "ionicons": "^6.0.3",
26
+    "lodash": "^4.17.21",
27
+    "notion-utils": "^6.15.6",
26
     "react": "^18.2.0",
28
     "react": "^18.2.0",
27
     "react-dom": "^18.2.0",
29
     "react-dom": "^18.2.0",
30
+    "react-notion-x": "^6.15.7",
28
     "react-router": "^5.2.0",
31
     "react-router": "^5.2.0",
29
     "react-router-dom": "^5.2.0",
32
     "react-router-dom": "^5.2.0",
30
     "react-scripts": "^5.0.0",
33
     "react-scripts": "^5.0.0",
72
   "devDependencies": {
75
   "devDependencies": {
73
     "@capacitor/cli": "4.4.0",
76
     "@capacitor/cli": "4.4.0",
74
     "@ionic/lab": "3.2.15",
77
     "@ionic/lab": "3.2.15",
78
+    "@types/lodash": "^4.14.191",
75
     "husky": "^8.0.0",
79
     "husky": "^8.0.0",
76
     "pretty-quick": "^3.1.3"
80
     "pretty-quick": "^3.1.3"
77
   },
81
   },

+ 3
- 1
src/App.tsx View File

34
 import AdviceListPage from "./pages/AdviceListPage";
34
 import AdviceListPage from "./pages/AdviceListPage";
35
 import LawListPage from "./pages/LawListPage";
35
 import LawListPage from "./pages/LawListPage";
36
 import ArticlePage from "./pages/ArticlePage";
36
 import ArticlePage from "./pages/ArticlePage";
37
+import "./theme/global.css";
38
+import "react-notion-x/src/styles.css";
37
 
39
 
38
 setupIonicReact();
40
 setupIonicReact();
39
 
41
 
51
           <Route path="/laws">
53
           <Route path="/laws">
52
             <LawListPage />
54
             <LawListPage />
53
           </Route>
55
           </Route>
54
-          <Route path="/article/:slug" component={ArticlePage} />
56
+          <Route path="/article/:articleId" component={ArticlePage} />
55
           <Route exact path="/">
57
           <Route exact path="/">
56
             <Redirect to="/home" />
58
             <Redirect to="/home" />
57
           </Route>
59
           </Route>

+ 13
- 0
src/components/TopicList.css View File

1
+.list {
2
+  border-radius: 20px;
3
+  padding: 0 0 0 0;
4
+}
5
+
6
+.ListItemText {
7
+  font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
8
+  width: 100%;
9
+}
10
+
11
+ion-list-header {
12
+  /* color: "primary"; */
13
+}

src/components/ListComponent.tsx → src/components/TopicList.tsx View File

1
 import { IonContent, IonItem, IonLabel, IonListHeader } from "@ionic/react";
1
 import { IonContent, IonItem, IonLabel, IonListHeader } from "@ionic/react";
2
-import "../components/ListComponent.css";
2
+import { Topic } from "../types";
3
 import SkeletonText from "./SkeletonText";
3
 import SkeletonText from "./SkeletonText";
4
+import "../components/TopicList.css";
4
 
5
 
5
 /**
6
 /**
6
  * This component creates a shimmering "skeleton" version of the list component.
7
  * This component creates a shimmering "skeleton" version of the list component.
7
  * It shows only when content isn't available yet (loading).
8
  * It shows only when content isn't available yet (loading).
8
  */
9
  */
9
-function ListComponentSkeleton({ items = 5 }: { items?: number }) {
10
+function ListSkeleton({ items = 5 }: { items?: number }) {
10
   return (
11
   return (
11
     <>
12
     <>
12
       {Array(items)
13
       {Array(items)
20
   );
21
   );
21
 }
22
 }
22
 
23
 
23
-const ListComponent: React.FC<{
24
+const TopicList: React.FC<{
24
   title?: string;
25
   title?: string;
25
-  laws: string[];
26
+  topics: Topic[];
26
   loading?: boolean;
27
   loading?: boolean;
27
-}> = (props) => {
28
-  const title = props.title;
29
-  const laws = props.laws;
30
-  const loading = props.loading ?? false;
31
-
28
+}> = ({ title, topics, loading }) => {
32
   return (
29
   return (
33
     <IonContent>
30
     <IonContent>
34
       {title ? (
31
       {title ? (
37
         </IonListHeader>
34
         </IonListHeader>
38
       ) : null}
35
       ) : null}
39
 
36
 
40
-      {loading && <ListComponentSkeleton />}
37
+      {loading && <ListSkeleton />}
41
 
38
 
42
       {!loading &&
39
       {!loading &&
43
-        laws.map((lawName: string) => {
40
+        topics.map((topic: Topic) => {
44
           return (
41
           return (
45
             <IonItem
42
             <IonItem
46
-              key={lawName}
47
-              routerLink={`/article/${encodeURIComponent(lawName)}`}
43
+              key={topic.blockId}
44
+              routerLink={`/article/${topic.blockId}`}
48
             >
45
             >
49
-              <p className="ListItemText">{lawName}</p>
46
+              <p className="ListItemText">{topic.name}</p>
50
             </IonItem>
47
             </IonItem>
51
           );
48
           );
52
         })}
49
         })}
53
     </IonContent>
50
     </IonContent>
54
   );
51
   );
55
 };
52
 };
56
-export default ListComponent;
53
+export default TopicList;

+ 22
- 4
src/pages/AdviceListPage.tsx View File

3
   IonHeader,
3
   IonHeader,
4
   IonPage,
4
   IonPage,
5
   IonTitle,
5
   IonTitle,
6
+  IonToast,
6
   IonToolbar,
7
   IonToolbar,
7
 } from "@ionic/react";
8
 } from "@ionic/react";
8
-import ListComponent from "../components/ListComponent";
9
+import { alertCircleOutline } from "ionicons/icons";
10
+import useSWR from "swr";
11
+import TopicList from "../components/TopicList";
12
+import { fetcher } from "../lib/api";
13
+import { Topic } from "../types";
9
 
14
 
10
 const AdviceListPage: React.FC = () => {
15
 const AdviceListPage: React.FC = () => {
16
+  const { data, error } = useSWR<Topic[]>(
17
+    "/api/getTopicList?type=advice",
18
+    fetcher
19
+  );
20
+  const isLoading = !data && !error;
21
+
11
   return (
22
   return (
12
     <IonPage>
23
     <IonPage>
13
       <IonHeader>
24
       <IonHeader>
14
         <IonToolbar>
25
         <IonToolbar>
15
-          <IonTitle>Consejos</IonTitle>
26
+          <IonTitle>Advice</IonTitle>
16
         </IonToolbar>
27
         </IonToolbar>
17
       </IonHeader>
28
       </IonHeader>
18
       <IonContent fullscreen>
29
       <IonContent fullscreen>
19
         <IonHeader collapse="condense">
30
         <IonHeader collapse="condense">
20
           <IonToolbar>
31
           <IonToolbar>
21
-            <IonTitle size="large">Consejos</IonTitle>
32
+            <IonTitle size="large">Advice</IonTitle>
22
           </IonToolbar>
33
           </IonToolbar>
23
         </IonHeader>
34
         </IonHeader>
24
-        <ListComponent laws={["Consejo 1", "Consejo 2"]} />
35
+        {data && <TopicList loading={isLoading} topics={data as Topic[]} />}
36
+        <IonToast
37
+          icon={alertCircleOutline}
38
+          color="danger"
39
+          isOpen={error !== undefined}
40
+          message="Failed to load items."
41
+          duration={1500}
42
+        />
25
       </IonContent>
43
       </IonContent>
26
     </IonPage>
44
     </IonPage>
27
   );
45
   );

+ 49
- 17
src/pages/ArticlePage.tsx View File

13
   IonToolbar,
13
   IonToolbar,
14
 } from "@ionic/react";
14
 } from "@ionic/react";
15
 import { RouteComponentProps } from "react-router";
15
 import { RouteComponentProps } from "react-router";
16
+import { NotionRenderer } from "react-notion-x";
17
+import { fetcher } from "../lib/api";
18
+import useSWR from "swr";
19
+import { formatDate, getBlockTitle, getPageProperty } from "notion-utils";
20
+import omit from "lodash/omit";
21
+import SkeletonText from "../components/SkeletonText";
16
 
22
 
17
-const ArticlePage: React.FC<RouteComponentProps<{ slug: string }>> = ({
23
+const ArticlePage: React.FC<RouteComponentProps<{ articleId: string }>> = ({
18
   match,
24
   match,
19
 }) => {
25
 }) => {
26
+  const { data, error } = useSWR<any>(
27
+    `/api/getTopic?id=${match.params.articleId}`,
28
+    fetcher
29
+  );
30
+  const isLoading = !data && !error;
31
+  const keys = Object.keys(data?.block || {});
32
+  const block = data?.block?.[keys[0]]?.value;
33
+  const title = data && getBlockTitle(block, data);
34
+  const date =
35
+    data &&
36
+    formatDate(block?.last_edited_time, {
37
+      month: "long",
38
+    });
39
+  const recordMap = data;
40
+  console.log(recordMap);
41
+
20
   return (
42
   return (
21
     <IonPage>
43
     <IonPage>
22
       <IonHeader>
44
       <IonHeader>
24
           <IonButtons slot="start">
46
           <IonButtons slot="start">
25
             <IonBackButton />
47
             <IonBackButton />
26
           </IonButtons>
48
           </IonButtons>
27
-          <IonTitle>{match.params.slug}</IonTitle>
49
+          <IonTitle>
50
+            {data ? getBlockTitle(block, data) : <SkeletonText />}
51
+          </IonTitle>
28
         </IonToolbar>
52
         </IonToolbar>
29
       </IonHeader>
53
       </IonHeader>
30
       <IonContent fullscreen>
54
       <IonContent fullscreen>
31
-        <IonCard>
32
-          <img
33
-            alt="Silhouette of mountains"
34
-            src="https://ionicframework.com/docs/img/demos/card-media.png"
35
-          />
36
-          <IonCardHeader>
37
-            <IonCardTitle>{match.params.slug}</IonCardTitle>
38
-            <IonCardSubtitle>Card Subtitle</IonCardSubtitle>
39
-          </IonCardHeader>
40
-
41
-          <IonCardContent>
42
-            Here's a small text description for the card content. Nothing more,
43
-            nothing less.
44
-          </IonCardContent>
45
-        </IonCard>
55
+        <IonCardHeader className="notion">
56
+          <IonCardTitle>{title ?? <SkeletonText />}</IonCardTitle>
57
+          <IonCardSubtitle>{date ?? <SkeletonText />}</IonCardSubtitle>
58
+        </IonCardHeader>
59
+        <IonCardContent>
60
+          {!recordMap && (
61
+            <div>
62
+              <SkeletonText />
63
+              <SkeletonText />
64
+              <SkeletonText />
65
+              <SkeletonText />
66
+              <SkeletonText />
67
+              <SkeletonText />
68
+            </div>
69
+          )}
70
+          {recordMap && (
71
+            <NotionRenderer
72
+              recordMap={recordMap}
73
+              fullPage={true}
74
+              darkMode={false}
75
+            />
76
+          )}
77
+        </IonCardContent>
46
       </IonContent>
78
       </IonContent>
47
     </IonPage>
79
     </IonPage>
48
   );
80
   );

+ 0
- 7
src/pages/Home.tsx View File

5
   IonTitle,
5
   IonTitle,
6
   IonToolbar,
6
   IonToolbar,
7
 } from "@ionic/react";
7
 } from "@ionic/react";
8
-import "../components/ListComponent";
9
-import ListComponent from "../components/ListComponent";
10
 
8
 
11
 const Home: React.FC = () => {
9
 const Home: React.FC = () => {
12
   return (
10
   return (
29
             printing and typesetting industry. Lorem Ipsum.
27
             printing and typesetting industry. Lorem Ipsum.
30
           </p>
28
           </p>
31
         </div>
29
         </div>
32
-
33
-        <ListComponent
34
-          laws={["Articulo 1", "Articulo 2"]}
35
-          title="Articulos Recientes"
36
-        />
37
       </IonContent>
30
       </IonContent>
38
     </IonPage>
31
     </IonPage>
39
   );
32
   );

+ 3
- 7
src/pages/LawListPage.tsx View File

9
 } from "@ionic/react";
9
 } from "@ionic/react";
10
 import { alertCircleOutline } from "ionicons/icons";
10
 import { alertCircleOutline } from "ionicons/icons";
11
 import useSWR from "swr";
11
 import useSWR from "swr";
12
-import ListComponent from "../components/ListComponent";
12
+import TopicList from "../components/TopicList";
13
 import { fetcher } from "../lib/api";
13
 import { fetcher } from "../lib/api";
14
 import { Topic } from "../types";
14
 import { Topic } from "../types";
15
 
15
 
33
             <IonTitle size="large">Leyes</IonTitle>
33
             <IonTitle size="large">Leyes</IonTitle>
34
           </IonToolbar>
34
           </IonToolbar>
35
         </IonHeader>
35
         </IonHeader>
36
-        <ListComponent
37
-          loading={isLoading}
38
-          laws={data ? data.map((item) => item.name) : []}
39
-        />
40
-
36
+        {data && <TopicList loading={isLoading} topics={data as Topic[]} />}
41
         <IonToast
37
         <IonToast
42
           icon={alertCircleOutline}
38
           icon={alertCircleOutline}
43
           color="danger"
39
           color="danger"
44
-          isOpen={error}
40
+          isOpen={error !== undefined}
45
           message="Failed to load items."
41
           message="Failed to load items."
46
           duration={1500}
42
           duration={1500}
47
         />
43
         />

+ 19
- 0
src/theme/global.css View File

1
+.notion-nav-header,
2
+.notion-title,
3
+.notion-header {
4
+  display: none !important;
5
+}
6
+
7
+.notion-page {
8
+  width: 100% !important;
9
+  padding: 0px 0px 0px 0px !important;
10
+}
11
+
12
+main.notion-page-no-cover {
13
+  padding-top: 0px !important;
14
+  margin-top: 0px !important;
15
+}
16
+
17
+.notion-blank {
18
+  display: none !important;
19
+}

+ 2
- 16
src/theme/variables.css View File

76
   --ion-color-light-tint: #f5f6f9;
76
   --ion-color-light-tint: #f5f6f9;
77
 }
77
 }
78
 
78
 
79
+/*
79
 @media (prefers-color-scheme: dark) {
80
 @media (prefers-color-scheme: dark) {
80
-  /*
81
-   * Dark Colors
82
-   * -------------------------------------------
83
-   */
84
-
85
   body {
81
   body {
86
     --ion-color-primary: #428cff;
82
     --ion-color-primary: #428cff;
87
     --ion-color-primary-rgb: 66,140,255;
83
     --ion-color-primary-rgb: 66,140,255;
147
     --ion-color-light-tint: #383a3e;
143
     --ion-color-light-tint: #383a3e;
148
   }
144
   }
149
 
145
 
150
-  /*
151
-   * iOS Dark Theme
152
-   * -------------------------------------------
153
-   */
154
-
155
   .ios body {
146
   .ios body {
156
     --ion-background-color: #000000;
147
     --ion-background-color: #000000;
157
     --ion-background-color-rgb: 0,0,0;
148
     --ion-background-color-rgb: 0,0,0;
190
     --ion-toolbar-border-color: var(--ion-color-step-250);
181
     --ion-toolbar-border-color: var(--ion-color-step-250);
191
   }
182
   }
192
 
183
 
193
-
194
-  /*
195
-   * Material Design Dark Theme
196
-   * -------------------------------------------
197
-   */
198
-
199
   .md body {
184
   .md body {
200
     --ion-background-color: #121212;
185
     --ion-background-color: #121212;
201
     --ion-background-color-rgb: 18,18,18;
186
     --ion-background-color-rgb: 18,18,18;
234
     --ion-card-background: #1e1e1e;
219
     --ion-card-background: #1e1e1e;
235
   }
220
   }
236
 }
221
 }
222
+*/