Browse Source

Merge branch 'matteing-sprint-1' of sergio.mattei/RenacerSocial into main

sergio.mattei 1 year ago
parent
commit
2213cd9df8

+ 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>

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


+ 5
- 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",
34
+    "swr": "^1.3.0",
31
     "typescript": "^4.1.3",
35
     "typescript": "^4.1.3",
32
     "web-vitals": "^0.2.4",
36
     "web-vitals": "^0.2.4",
33
     "workbox-background-sync": "^5.1.4",
37
     "workbox-background-sync": "^5.1.4",
71
   "devDependencies": {
75
   "devDependencies": {
72
     "@capacitor/cli": "4.4.0",
76
     "@capacitor/cli": "4.4.0",
73
     "@ionic/lab": "3.2.15",
77
     "@ionic/lab": "3.2.15",
78
+    "@types/lodash": "^4.14.191",
74
     "husky": "^8.0.0",
79
     "husky": "^8.0.0",
75
     "pretty-quick": "^3.1.3"
80
     "pretty-quick": "^3.1.3"
76
   },
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>

+ 9
- 6
src/components/ListComponent.css View File

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

+ 0
- 27
src/components/ListComponent.tsx View File

1
-import { IonItem, IonLabel, IonList, IonListHeader } from "@ionic/react";
2
-import "../components/ListComponent.css";
3
-
4
-const ListComponent: React.FC<{ title?: string; items: string[] }> = (
5
-  props
6
-) => {
7
-  const title = props.title;
8
-  const items = props.items;
9
-  return (
10
-    <IonList>
11
-      {title ? (
12
-        <IonListHeader>
13
-          <IonLabel>{title}</IonLabel>
14
-        </IonListHeader>
15
-      ) : null}
16
-
17
-      {items.map((itemName: string) => {
18
-        return (
19
-          <IonItem routerLink={`/article/${encodeURIComponent(itemName)}`}>
20
-            <p className="ListItemText">{itemName}</p>
21
-          </IonItem>
22
-        );
23
-      })}
24
-    </IonList>
25
-  );
26
-};
27
-export default ListComponent;

+ 12
- 0
src/components/SkeletonText.tsx View File

1
+import { IonSkeletonText } from "@ionic/react";
2
+import React from "react";
3
+
4
+function skeletonRange() {
5
+  return Math.floor(Math.random() * (75 - 25 + 1)) + 25;
6
+}
7
+
8
+export default function SkeletonText() {
9
+  return (
10
+    <IonSkeletonText animated={true} style={{ width: `${skeletonRange()}%` }} />
11
+  );
12
+}

+ 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
+}

+ 53
- 0
src/components/TopicList.tsx View File

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

+ 1
- 0
src/config.ts View File

1
+export const API_URL = process.env.API_URL ?? "http://localhost:3000";

+ 4
- 0
src/lib/api.ts View File

1
+import { API_URL } from "../config";
2
+
3
+export const fetcher = (url: string) =>
4
+  fetch(`${API_URL}${url}`).then((r) => r.json());

+ 22
- 17
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 DropdownComponent from "../components/DropdownComponent";
9
-import "../types";
10
-
11
-const categories = [
12
-  {
13
-    name: "Paternidad",
14
-    listItems: ["Consejo de Paternidad 1", "Consejo de Paternidad 2"],
15
-  },
16
-  {
17
-    name: "Abuso Sexual",
18
-    listItems: ["Consejo de Abuso Sexual 1", "Consejo de Abuso Sexual 2"],
19
-  },
20
-];
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";
21
 
14
 
22
 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
+
23
   return (
22
   return (
24
     <IonPage>
23
     <IonPage>
25
       <IonHeader>
24
       <IonHeader>
26
         <IonToolbar>
25
         <IonToolbar>
27
-          <IonTitle>Consejos</IonTitle>
26
+          <IonTitle>Advice</IonTitle>
28
         </IonToolbar>
27
         </IonToolbar>
29
       </IonHeader>
28
       </IonHeader>
30
       <IonContent fullscreen>
29
       <IonContent fullscreen>
31
         <IonHeader collapse="condense">
30
         <IonHeader collapse="condense">
32
           <IonToolbar>
31
           <IonToolbar>
33
-            <IonTitle size="large">Consejos</IonTitle>
32
+            <IonTitle size="large">Advice</IonTitle>
34
           </IonToolbar>
33
           </IonToolbar>
35
         </IonHeader>
34
         </IonHeader>
36
-
37
-        <DropdownComponent category={categories} />
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
+        />
38
       </IonContent>
43
       </IonContent>
39
     </IonPage>
44
     </IonPage>
40
   );
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
-          items={["Articulo 1", "Articulo 2"]}
35
-          title="Articulos Recientes"
36
-        />
37
       </IonContent>
30
       </IonContent>
38
     </IonPage>
31
     </IonPage>
39
   );
32
   );

+ 21
- 2
src/pages/LawListPage.tsx View File

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

+ 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
+*/

+ 8
- 4
src/types.ts View File

1
-export interface Category {
2
-    name: string;
3
-    listItems: string[];
4
-}
1
+export interface Topic {
2
+  blockId: string;
3
+  name: string;
4
+}
5
+
6
+export interface Error {
7
+  status: string;
8
+}