Browse Source

Merge branch 'main' of https://git.ccom.uprrp.edu/sergio.mattei/RenacerSocial into eliam-sprint-1

# Conflicts:
#	src/components/DropdownComponent.tsx
#	src/components/ListComponent.tsx
#	src/pages/AdviceListPage.tsx
eliam.ruiz 1 year ago
parent
commit
13edd3582f

+ 15
- 0
.expo/README.md View File

@@ -0,0 +1,15 @@
1
+> Why do I have a folder named ".expo" in my project?
2
+
3
+The ".expo" folder is created when an Expo project is started using "expo start" command.
4
+
5
+> What do the files contain?
6
+
7
+- "devices.json": contains information about devices that have recently opened this project. This is used to populate the "Development sessions" list in your development builds.
8
+- "packager-info.json": contains port numbers and process PIDs that are used to serve the application to the mobile device/simulator.
9
+- "settings.json": contains the server configuration that is used to serve the application manifest.
10
+
11
+> Should I commit the ".expo" folder?
12
+
13
+No, you should not share the ".expo" folder. It does not contain any information that is relevant for other developers working on the project, it is specific to your machine.
14
+
15
+Upon project creation, the ".expo" folder is already added to your ".gitignore" file.

+ 8
- 0
.expo/settings.json View File

@@ -0,0 +1,8 @@
1
+{
2
+  "hostType": "lan",
3
+  "lanType": "ip",
4
+  "dev": true,
5
+  "minify": false,
6
+  "urlRandomness": null,
7
+  "https": false
8
+}

+ 5
- 5
capacitor.config.ts View File

@@ -1,10 +1,10 @@
1
-import { CapacitorConfig } from '@capacitor/cli';
1
+import { CapacitorConfig } from "@capacitor/cli";
2 2
 
3 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 10
 export default config;

+ 10
- 0
cypress.config.ts View File

@@ -0,0 +1,10 @@
1
+import { defineConfig } from "cypress";
2
+
3
+export default defineConfig({
4
+  e2e: {
5
+    baseUrl: "http://localhost:3001",
6
+    setupNodeEvents(on, config) {
7
+      // implement node event listeners here
8
+    },
9
+  },
10
+});

+ 2
- 0
cypress/e2e/eduardo.cy.ts View File

@@ -0,0 +1,2 @@
1
+// I will E2E test the article page.
2
+// User visits home -> User clicks on "laws or advice" -> List renders with 200

+ 50
- 0
cypress/e2e/eliam.cy.ts View File

@@ -0,0 +1,50 @@
1
+// I will E2E test the article page.
2
+// User visits home -> User clicks on "laws or advice" -> Article page contains the title
3
+
4
+
5
+describe("Our app", () => {
6
+    beforeEach(() => {
7
+      cy.intercept(
8
+        { pathname: "/api/getTopicList" },
9
+        {
10
+          fixture: "topics",
11
+        }
12
+      ).as("getTopicList");
13
+  
14
+      cy.intercept(
15
+        { pathname: "/api/getTopic" },
16
+        {
17
+          fixture: "article",
18
+        }
19
+      ).as("getTopic");
20
+  
21
+      cy.viewport("iphone-x");
22
+      cy.visit("/");
23
+    });
24
+
25
+    it("shows the home page", () => {
26
+        cy.visit("/");
27
+        cy.contains("Bienvenidos");
28
+    });
29
+
30
+    it("clicks and loads the advice tab", () => {
31
+        cy.get("#tab-button-advice").first().click();
32
+        cy.wait("@getTopicList");
33
+    })
34
+    it("clicks and loads advice info", () => {
35
+        cy.get("#tab-button-advice").first().click();
36
+        cy.wait("@getTopicList");
37
+        cy.get(".item").first().click();
38
+        cy.wait("@getTopic");
39
+        cy.contains("Adopción");
40
+        cy.contains("Enmendado en el 1939");
41
+    })
42
+    it("clicks and loads law info", () => {
43
+        cy.get("#tab-button-laws").first().click();
44
+        cy.wait("@getTopicList");
45
+        cy.get(".item").first().click();
46
+        cy.wait("@getTopic");
47
+        cy.contains("Adopción");
48
+        cy.contains("Enmendado en el 1939");
49
+    })
50
+})

+ 56
- 0
cypress/e2e/sergio.cy.ts View File

@@ -0,0 +1,56 @@
1
+describe("Our app", () => {
2
+  beforeEach(() => {
3
+    cy.intercept(
4
+      { pathname: "/api/getTopicList" },
5
+      {
6
+        fixture: "topics",
7
+      }
8
+    ).as("getTopicList");
9
+
10
+    cy.intercept(
11
+      { pathname: "/api/getTopic" },
12
+      {
13
+        fixture: "article",
14
+      }
15
+    ).as("getTopic");
16
+
17
+    cy.viewport("iphone-x");
18
+    cy.visit("/");
19
+  });
20
+
21
+  it("shows the home page", () => {
22
+    cy.visit("/");
23
+    cy.contains("Bienvenidos");
24
+  });
25
+
26
+  it("shows a video in the home page", () => {
27
+    cy.visit("/");
28
+    cy.get("iframe").should("be.visible");
29
+  });
30
+
31
+  it("shows the about page", () => {
32
+    cy.visit("/about");
33
+    cy.contains("Renacer Social");
34
+  });
35
+
36
+  it("shows the individual service pages", () => {
37
+    cy.visit("/about");
38
+    cy.get(".item").first().click();
39
+    cy.contains("Casita de Paz");
40
+  });
41
+
42
+  it("shows the advice pages", () => {
43
+    cy.visit("/advice");
44
+    // Wait until the request to the intercept is completed.
45
+    cy.wait("@getTopicList");
46
+    cy.get(".item").first().click();
47
+    cy.wait("@getTopic");
48
+  });
49
+
50
+  it("allows exiting the individual service pages", () => {
51
+    cy.visit("/about");
52
+    cy.get(".item").first().click();
53
+    cy.get(".back-button-has-icon-only").first().click();
54
+    cy.contains("Servicios");
55
+  });
56
+});

+ 1643
- 0
cypress/fixtures/article.json
File diff suppressed because it is too large
View File


+ 14
- 0
cypress/fixtures/topics.json View File

@@ -0,0 +1,14 @@
1
+[
2
+  {
3
+    "blockId": "07ffbf26-babe-4c6a-b046-c7eb6d5c1a7f",
4
+    "name": "Pasa tiempo con tus hijos"
5
+  },
6
+  {
7
+    "blockId": "754d8ef3-14ae-4af6-a8c3-4ac4ede4d49d",
8
+    "name": "10 consejos para un matrimonio feliz"
9
+  },
10
+  {
11
+    "blockId": "d9fbaf7c-8f89-43b3-945c-927ed90e098b",
12
+    "name": "Busca ayuda si eres abusado/abusada"
13
+  }
14
+]

+ 37
- 0
cypress/support/commands.ts View File

@@ -0,0 +1,37 @@
1
+/// <reference types="cypress" />
2
+// ***********************************************
3
+// This example commands.ts shows you how to
4
+// create various custom commands and overwrite
5
+// existing commands.
6
+//
7
+// For more comprehensive examples of custom
8
+// commands please read more here:
9
+// https://on.cypress.io/custom-commands
10
+// ***********************************************
11
+//
12
+//
13
+// -- This is a parent command --
14
+// Cypress.Commands.add('login', (email, password) => { ... })
15
+//
16
+//
17
+// -- This is a child command --
18
+// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
19
+//
20
+//
21
+// -- This is a dual command --
22
+// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
23
+//
24
+//
25
+// -- This will overwrite an existing command --
26
+// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
27
+//
28
+// declare global {
29
+//   namespace Cypress {
30
+//     interface Chainable {
31
+//       login(email: string, password: string): Chainable<void>
32
+//       drag(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
33
+//       dismiss(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
34
+//       visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element>
35
+//     }
36
+//   }
37
+// }

+ 20
- 0
cypress/support/e2e.ts View File

@@ -0,0 +1,20 @@
1
+// ***********************************************************
2
+// This example support/e2e.ts is processed and
3
+// loaded automatically before your test files.
4
+//
5
+// This is a great place to put global configuration and
6
+// behavior that modifies Cypress.
7
+//
8
+// You can change the location of this file or turn off
9
+// automatically serving support files with the
10
+// 'supportFile' configuration option.
11
+//
12
+// You can read more here:
13
+// https://on.cypress.io/configuration
14
+// ***********************************************************
15
+
16
+// Import commands.js using ES2015 syntax:
17
+import "./commands";
18
+
19
+// Alternatively you can use CommonJS syntax:
20
+// require('./commands')

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

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

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

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

+ 2859
- 30
package-lock.json
File diff suppressed because it is too large
View File


+ 9
- 2
package.json View File

@@ -23,11 +23,15 @@
23 23
     "@types/react-router-dom": "^5.1.7",
24 24
     "history": "^4.9.0",
25 25
     "ionicons": "^6.0.3",
26
+    "lodash": "^4.17.21",
27
+    "notion-utils": "^6.15.6",
26 28
     "react": "^18.2.0",
27 29
     "react-dom": "^18.2.0",
30
+    "react-notion-x": "^6.15.7",
28 31
     "react-router": "^5.2.0",
29 32
     "react-router-dom": "^5.2.0",
30 33
     "react-scripts": "^5.0.0",
34
+    "swr": "^1.3.0",
31 35
     "typescript": "^4.1.3",
32 36
     "web-vitals": "^0.2.4",
33 37
     "workbox-background-sync": "^5.1.4",
@@ -44,11 +48,12 @@
44 48
     "workbox-streams": "^5.1.4"
45 49
   },
46 50
   "scripts": {
47
-    "start": "react-scripts start",
51
+    "start": "PORT=3001 react-scripts start",
48 52
     "build": "react-scripts build",
49 53
     "test": "react-scripts test --transformIgnorePatterns 'node_modules/(?!(@ionic/react|@ionic/react-router|@ionic/core|@stencil/core|ionicons)/)'",
50 54
     "eject": "react-scripts eject",
51
-    "prepare": "husky install"
55
+    "prepare": "husky install",
56
+    "cypress": "cypress open"
52 57
   },
53 58
   "eslintConfig": {
54 59
     "extends": [
@@ -71,6 +76,8 @@
71 76
   "devDependencies": {
72 77
     "@capacitor/cli": "4.4.0",
73 78
     "@ionic/lab": "3.2.15",
79
+    "@types/lodash": "^4.14.191",
80
+    "cypress": "^12.1.0",
74 81
     "husky": "^8.0.0",
75 82
     "pretty-quick": "^3.1.3"
76 83
   },

BIN
public/assets/icon/about.png View File


BIN
public/assets/icon/articule.png View File


BIN
public/assets/icon/law.jpeg View File


+ 49
- 4
src/App.tsx View File

@@ -10,7 +10,14 @@ import {
10 10
   setupIonicReact,
11 11
 } from "@ionic/react";
12 12
 import { IonReactRouter } from "@ionic/react-router";
13
-import { ellipse, home } from "ionicons/icons";
13
+import {
14
+  ellipse,
15
+  informationCircleSharp,
16
+  bookSharp,
17
+  appsSharp,
18
+  home,
19
+  peopleCircleSharp,
20
+} from "ionicons/icons";
14 21
 import Home from "./pages/Home";
15 22
 
16 23
 /* Core CSS required for Ionic components to work properly */
@@ -34,6 +41,13 @@ import "./theme/variables.css";
34 41
 import AdviceListPage from "./pages/AdviceListPage";
35 42
 import LawListPage from "./pages/LawListPage";
36 43
 import ArticlePage from "./pages/ArticlePage";
44
+import "./theme/global.css";
45
+import "react-notion-x/src/styles.css";
46
+import AboutListPage from "./pages/AboutListPage";
47
+import CasitaPage from "./pages/services/CasitaPage";
48
+import CrecemosPage from "./pages/services/CrecemosPage";
49
+import LazosPage from "./pages/services/LazosPage";
50
+import SupervisadasPage from "./pages/services/SupervisadasPage";
37 51
 
38 52
 setupIonicReact();
39 53
 
@@ -45,30 +59,61 @@ const App: React.FC = () => (
45 59
           <Route exact path="/home">
46 60
             <Home />
47 61
           </Route>
62
+
48 63
           <Route path="/advice">
49 64
             <AdviceListPage />
50 65
           </Route>
66
+
51 67
           <Route path="/laws">
52 68
             <LawListPage />
53 69
           </Route>
54
-          <Route path="/article/:slug" component={ArticlePage} />
70
+
71
+          <Route path="/article/:articleId" component={ArticlePage} />
72
+
73
+          <Route path="/services/lazos">
74
+            <LazosPage />
75
+          </Route>
76
+
77
+          <Route path="/services/crecemos">
78
+            <CrecemosPage />
79
+          </Route>
80
+
81
+          <Route path="/services/supervisadas">
82
+            <SupervisadasPage />
83
+          </Route>
84
+
85
+          <Route path="/about">
86
+            <AboutListPage />
87
+          </Route>
88
+
89
+          <Route path="/services/casita">
90
+            <CasitaPage />
91
+          </Route>
92
+
55 93
           <Route exact path="/">
56 94
             <Redirect to="/home" />
57 95
           </Route>
58 96
         </IonRouterOutlet>
97
+
59 98
         <IonTabBar slot="bottom">
60 99
           <IonTabButton tab="home" href="/home">
61 100
             <IonIcon icon={home} />
62 101
             <IonLabel>Home</IonLabel>
63 102
           </IonTabButton>
103
+
64 104
           <IonTabButton tab="advice" href="/advice">
65
-            <IonIcon icon={ellipse} />
105
+            <IonIcon icon={peopleCircleSharp} />
66 106
             <IonLabel>Advice</IonLabel>
67 107
           </IonTabButton>
108
+
68 109
           <IonTabButton tab="laws" href="/laws">
69
-            <IonIcon icon={ellipse} />
110
+            <IonIcon icon={bookSharp} />
70 111
             <IonLabel>Laws</IonLabel>
71 112
           </IonTabButton>
113
+          <IonTabButton tab="about" href="/about">
114
+            <IonIcon icon={informationCircleSharp} />
115
+            <IonLabel>About</IonLabel>
116
+          </IonTabButton>
72 117
         </IonTabBar>
73 118
       </IonTabs>
74 119
     </IonReactRouter>

+ 0
- 3
src/components/DropdownComponent.css View File

@@ -1,3 +0,0 @@
1
-.accordion-list-container {
2
-    width: 100%;
3
-}

+ 0
- 34
src/components/DropdownComponent.tsx View File

@@ -1,34 +0,0 @@
1
-import {
2
-  IonAccordion,
3
-  IonAccordionGroup,
4
-  IonItem,
5
-  IonLabel,
6
-} from "@ionic/react";
7
-import ListComponent from "../components/ListComponent";
8
-import { Category } from "../types";
9
-import "./DropdownComponent.css";
10
-
11
-const DropdownComponent: React.FC<{
12
-  category: Category[];
13
-}> = (props) => {
14
-  const category = props.category;
15
-  return (
16
-    <IonAccordionGroup>
17
-      {category.map((catItem: Category, index) => {
18
-        return (
19
-          <IonAccordion value={`${index}`}>
20
-            <IonItem slot="header" color="light">
21
-              <IonLabel>{catItem.name}</IonLabel>
22
-            </IonItem>
23
-            <IonItem slot="content">
24
-              <div className="accordion-list-container">
25
-                <ListComponent items={catItem.listItems} />
26
-              </div>
27
-            </IonItem>
28
-          </IonAccordion>
29
-        );
30
-      })}
31
-    </IonAccordionGroup>
32
-  );
33
-};
34
-export default DropdownComponent;

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

@@ -1,10 +1,13 @@
1
-
2 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 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,27 +0,0 @@
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

@@ -0,0 +1,12 @@
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

@@ -0,0 +1,13 @@
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
+}

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

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

+ 1
- 0
src/config.ts View File

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

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

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

+ 69
- 0
src/pages/AboutListPage.tsx View File

@@ -0,0 +1,69 @@
1
+import {
2
+  IonContent,
3
+  IonHeader,
4
+  IonImg,
5
+  IonItem,
6
+  IonLabel,
7
+  IonList,
8
+  IonListHeader,
9
+  IonPage,
10
+  IonTitle,
11
+  IonToolbar,
12
+} from "@ionic/react";
13
+
14
+const AboutListPage: React.FC = () => {
15
+  return (
16
+    <IonPage>
17
+      <IonHeader>
18
+        <IonToolbar>
19
+          <IonTitle>About</IonTitle>
20
+        </IonToolbar>
21
+      </IonHeader>
22
+      <IonContent fullscreen>
23
+        <IonHeader collapse="condense">
24
+          <IonToolbar>
25
+            <IonTitle size="large">About</IonTitle>
26
+          </IonToolbar>
27
+        </IonHeader>
28
+        <IonContent className="ion-padding">
29
+          <div className="video">
30
+            <iframe
31
+              src="https://www.youtube.com/embed/MRo8FR2l6VM"
32
+              title="YouTube video player"
33
+              allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
34
+            ></iframe>
35
+          </div>
36
+          <h5>Bienvenidos</h5>
37
+          <p>
38
+            Renacer Social ha iniciado una campaña de orientación a la comunidad
39
+            para que los niños y niñas puedan denunciar su situación de
40
+            maltrato. Con una seña discreta que consta de una “X” formada frente
41
+            al torso con los dedos índice, el menor puede dar alerta de su
42
+            situación de maltrato. Es responsabilidad de todos denunciar la
43
+            posible situación de maltrato si vemos a un menor realizar dicha
44
+            seña.
45
+          </p>
46
+          <IonList>
47
+            <IonListHeader>
48
+              <IonLabel>Servicios</IonLabel>
49
+            </IonListHeader>
50
+            <IonItem routerLink={`/services/casita`}>
51
+              <p className="ListItemText">Casita de Paz</p>
52
+            </IonItem>
53
+            <IonItem routerLink={`/services/Crecemos`}>
54
+              <p className="ListItemText">Crecemos</p>
55
+            </IonItem>
56
+            <IonItem routerLink={`/services/Lazos`}>
57
+              <p className="ListItemText">Proyecto Lazos</p>
58
+            </IonItem>
59
+            <IonItem routerLink={`/services/Supervisadas`}>
60
+              <p className="ListItemText">Visitas Supervisadas</p>
61
+            </IonItem>
62
+          </IonList>
63
+        </IonContent>
64
+      </IonContent>
65
+    </IonPage>
66
+  );
67
+};
68
+
69
+export default AboutListPage;

+ 45
- 17
src/pages/AdviceListPage.tsx View File

@@ -3,40 +3,68 @@ import {
3 3
   IonHeader,
4 4
   IonPage,
5 5
   IonTitle,
6
+  IonToast,
6 7
   IonToolbar,
7 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 15
 const AdviceListPage: React.FC = () => {
16
+  const { data: paternidad, error: paternidadError } = useSWR<Topic[]>(
17
+    "/api/getTopicList?type=advice&category=Paternidad",
18
+    fetcher
19
+  );
20
+  const { data: matrimonio, error: matrimonioError } = useSWR<Topic[]>(
21
+    "/api/getTopicList?type=advice&category=Matrimonio",
22
+    fetcher
23
+  );
24
+  const { data: abuso, error: abusoError } = useSWR<Topic[]>(
25
+    "/api/getTopicList?type=advice&category=Abuso%20Sexual",
26
+    fetcher
27
+  );
28
+  const error = paternidadError || matrimonioError || abusoError;
29
+
23 30
   return (
24 31
     <IonPage>
25 32
       <IonHeader>
26 33
         <IonToolbar>
27
-          <IonTitle>Consejos</IonTitle>
34
+          <IonTitle>Advice</IonTitle>
28 35
         </IonToolbar>
29 36
       </IonHeader>
30 37
       <IonContent fullscreen>
31 38
         <IonHeader collapse="condense">
32 39
           <IonToolbar>
33
-            <IonTitle size="large">Consejos</IonTitle>
40
+            <IonTitle size="large">Advice</IonTitle>
34 41
           </IonToolbar>
35 42
         </IonHeader>
36
-        <DropdownComponent category={categories} />
43
+        <TopicList
44
+          title="Paternidad"
45
+          loading={!paternidad && !paternidadError}
46
+          topics={paternidad ? (paternidad as Topic[]) : []}
47
+        />
48
+        <TopicList
49
+          title="Matrimonio"
50
+          loading={!matrimonio && !matrimonioError}
51
+          topics={matrimonio ? (matrimonio as Topic[]) : []}
52
+        />
53
+        <TopicList
54
+          title="Abuso Sexual"
55
+          loading={!abuso && !abusoError}
56
+          topics={abuso ? (abuso as Topic[]) : []}
57
+        />
58
+        <IonToast
59
+          icon={alertCircleOutline}
60
+          color="danger"
61
+          isOpen={error !== undefined}
62
+          message="Failed to load items."
63
+          duration={1500}
64
+        />
37 65
       </IonContent>
38 66
     </IonPage>
39 67
   );
40 68
 };
41 69
 
42
-export default AdviceListPage;
70
+export default AdviceListPage;

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

@@ -13,10 +13,31 @@ import {
13 13
   IonToolbar,
14 14
 } from "@ionic/react";
15 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 SkeletonText from "../components/SkeletonText";
16 21
 
17
-const ArticlePage: React.FC<RouteComponentProps<{ slug: string }>> = ({
22
+const ArticlePage: React.FC<RouteComponentProps<{ articleId: string }>> = ({
18 23
   match,
19 24
 }) => {
25
+  const { data, error } = useSWR<any>(
26
+    `/api/getTopic?id=${match.params.articleId}`,
27
+    fetcher
28
+  );
29
+  const isLoading = !data && !error;
30
+  const keys = Object.keys(data?.block || {});
31
+  const block = data?.block?.[keys[0]]?.value;
32
+  const title = data && getBlockTitle(block, data);
33
+  const date =
34
+    data &&
35
+    formatDate(block?.last_edited_time, {
36
+      month: "long",
37
+    });
38
+  const recordMap = data;
39
+  console.log(recordMap);
40
+
20 41
   return (
21 42
     <IonPage>
22 43
       <IonHeader>
@@ -24,25 +45,35 @@ const ArticlePage: React.FC<RouteComponentProps<{ slug: string }>> = ({
24 45
           <IonButtons slot="start">
25 46
             <IonBackButton />
26 47
           </IonButtons>
27
-          <IonTitle>{match.params.slug}</IonTitle>
48
+          <IonTitle>
49
+            {data ? getBlockTitle(block, data) : <SkeletonText />}
50
+          </IonTitle>
28 51
         </IonToolbar>
29 52
       </IonHeader>
30 53
       <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>
54
+        <IonCardHeader className="notion">
55
+          <IonCardTitle>{title ?? <SkeletonText />}</IonCardTitle>
56
+          <IonCardSubtitle>{date ?? <SkeletonText />}</IonCardSubtitle>
57
+        </IonCardHeader>
58
+        <IonCardContent>
59
+          {!recordMap && (
60
+            <div>
61
+              <SkeletonText />
62
+              <SkeletonText />
63
+              <SkeletonText />
64
+              <SkeletonText />
65
+              <SkeletonText />
66
+              <SkeletonText />
67
+            </div>
68
+          )}
69
+          {recordMap && (
70
+            <NotionRenderer
71
+              recordMap={recordMap}
72
+              fullPage={true}
73
+              darkMode={false}
74
+            />
75
+          )}
76
+        </IonCardContent>
46 77
       </IonContent>
47 78
     </IonPage>
48 79
   );

+ 15
- 8
src/pages/Home.tsx View File

@@ -5,8 +5,6 @@ import {
5 5
   IonTitle,
6 6
   IonToolbar,
7 7
 } from "@ionic/react";
8
-import "../components/ListComponent";
9
-import ListComponent from "../components/ListComponent";
10 8
 
11 9
 const Home: React.FC = () => {
12 10
   return (
@@ -23,17 +21,26 @@ const Home: React.FC = () => {
23 21
           </IonToolbar>
24 22
         </IonHeader>
25 23
         <div className="ion-padding">
26
-          <h5>Bienvenidos</h5>
24
+          <div className="video">
25
+            <iframe
26
+              src="https://www.youtube.com/embed/xCtgekNpxI4"
27
+              title="YouTube video player"
28
+              allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
29
+            ></iframe>
30
+          </div>
31
+          <h2>Bienvenidos</h2>
27 32
           <p>
28 33
             Somos una organización Lorem Ipsum is simply dummy text of the
29 34
             printing and typesetting industry. Lorem Ipsum.
30 35
           </p>
31
-        </div>
32 36
 
33
-        <ListComponent
34
-          items={["Articulo 1", "Articulo 2"]}
35
-          title="Articulos Recientes"
36
-        />
37
+          <h3>Si se encuentra en peligro llame al 9-1-1 o al 787-343-2020</h3>
38
+          <p>
39
+            Si usted se encuentra en peligro inminente, llame a la Policía de
40
+            Puerto Rico al 9-1-1 o al 787-343-2020. Debe buscar protección para
41
+            usted y los suyos de manera inmediata. NO ESPERE.
42
+          </p>
43
+        </div>
37 44
       </IonContent>
38 45
     </IonPage>
39 46
   );

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

@@ -3,11 +3,22 @@ import {
3 3
   IonHeader,
4 4
   IonPage,
5 5
   IonTitle,
6
+  IonToast,
6 7
   IonToolbar,
7 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 15
 const LawListPage: React.FC = () => {
16
+  const { data, error } = useSWR<Topic[]>(
17
+    "/api/getTopicList?type=laws",
18
+    fetcher
19
+  );
20
+  const isLoading = !data && !error;
21
+
11 22
   return (
12 23
     <IonPage>
13 24
       <IonHeader>
@@ -21,7 +32,14 @@ const LawListPage: React.FC = () => {
21 32
             <IonTitle size="large">Leyes</IonTitle>
22 33
           </IonToolbar>
23 34
         </IonHeader>
24
-        <ListComponent items={["Law 1", "Law 2"]} />
35
+        <TopicList loading={isLoading} topics={data ? (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 43
       </IonContent>
26 44
     </IonPage>
27 45
   );

+ 46
- 0
src/pages/services/CasitaPage.tsx View File

@@ -0,0 +1,46 @@
1
+import {
2
+  IonBackButton,
3
+  IonButtons,
4
+  IonCard,
5
+  IonCardContent,
6
+  IonCardHeader,
7
+  IonCardSubtitle,
8
+  IonCardTitle,
9
+  IonContent,
10
+  IonHeader,
11
+  IonPage,
12
+  IonTitle,
13
+  IonToolbar,
14
+} from "@ionic/react";
15
+import { RouteComponentProps } from "react-router";
16
+
17
+const CasitaPage: React.FC = () => {
18
+  return (
19
+    <IonPage>
20
+      <IonHeader>
21
+        <IonToolbar>
22
+          <IonButtons slot="start">
23
+            <IonBackButton />
24
+          </IonButtons>
25
+          <IonTitle>Servicios</IonTitle>
26
+        </IonToolbar>
27
+      </IonHeader>
28
+      <IonContent fullscreen>
29
+        <IonCard>
30
+          <img
31
+            alt="Silhouette of mountains"
32
+            src="https://ionicframework.com/docs/img/demos/card-media.png"
33
+          />
34
+          <IonCardHeader>
35
+            <IonCardTitle>Casita de Paz</IonCardTitle>
36
+            <IonCardSubtitle></IonCardSubtitle>
37
+          </IonCardHeader>
38
+
39
+          <IonCardContent>Romper ciclos negativos</IonCardContent>
40
+        </IonCard>
41
+      </IonContent>
42
+    </IonPage>
43
+  );
44
+};
45
+
46
+export default CasitaPage;

+ 46
- 0
src/pages/services/CrecemosPage.tsx View File

@@ -0,0 +1,46 @@
1
+import {
2
+  IonBackButton,
3
+  IonButtons,
4
+  IonCard,
5
+  IonCardContent,
6
+  IonCardHeader,
7
+  IonCardSubtitle,
8
+  IonCardTitle,
9
+  IonContent,
10
+  IonHeader,
11
+  IonPage,
12
+  IonTitle,
13
+  IonToolbar,
14
+} from "@ionic/react";
15
+import { RouteComponentProps } from "react-router";
16
+
17
+const CrecemosPage: React.FC = () => {
18
+  return (
19
+    <IonPage>
20
+      <IonHeader>
21
+        <IonToolbar>
22
+          <IonButtons slot="start">
23
+            <IonBackButton />
24
+          </IonButtons>
25
+          <IonTitle>Servicios</IonTitle>
26
+        </IonToolbar>
27
+      </IonHeader>
28
+      <IonContent fullscreen>
29
+        <IonCard>
30
+          <img
31
+            alt="Silhouette of mountains"
32
+            src="https://ionicframework.com/docs/img/demos/card-media.png"
33
+          />
34
+          <IonCardHeader>
35
+            <IonCardTitle>Crecemos</IonCardTitle>
36
+            <IonCardSubtitle></IonCardSubtitle>
37
+          </IonCardHeader>
38
+
39
+          <IonCardContent>Educacion Co-parental</IonCardContent>
40
+        </IonCard>
41
+      </IonContent>
42
+    </IonPage>
43
+  );
44
+};
45
+
46
+export default CrecemosPage;

+ 46
- 0
src/pages/services/LazosPage.tsx View File

@@ -0,0 +1,46 @@
1
+import {
2
+  IonBackButton,
3
+  IonButtons,
4
+  IonCard,
5
+  IonCardContent,
6
+  IonCardHeader,
7
+  IonCardSubtitle,
8
+  IonCardTitle,
9
+  IonContent,
10
+  IonHeader,
11
+  IonPage,
12
+  IonTitle,
13
+  IonToolbar,
14
+} from "@ionic/react";
15
+import { RouteComponentProps } from "react-router";
16
+
17
+const LazosPage: React.FC = () => {
18
+  return (
19
+    <IonPage>
20
+      <IonHeader>
21
+        <IonToolbar>
22
+          <IonButtons slot="start">
23
+            <IonBackButton />
24
+          </IonButtons>
25
+          <IonTitle>Servicios</IonTitle>
26
+        </IonToolbar>
27
+      </IonHeader>
28
+      <IonContent fullscreen>
29
+        <IonCard>
30
+          <img
31
+            alt="Silhouette of mountains"
32
+            src="https://ionicframework.com/docs/img/demos/card-media.png"
33
+          />
34
+          <IonCardHeader>
35
+            <IonCardTitle>Proyecto de Lazos</IonCardTitle>
36
+            <IonCardSubtitle></IonCardSubtitle>
37
+          </IonCardHeader>
38
+
39
+          <IonCardContent>Maltrato de Parejas</IonCardContent>
40
+        </IonCard>
41
+      </IonContent>
42
+    </IonPage>
43
+  );
44
+};
45
+
46
+export default LazosPage;

+ 48
- 0
src/pages/services/SupervisadasPage.tsx View File

@@ -0,0 +1,48 @@
1
+import {
2
+  IonBackButton,
3
+  IonButtons,
4
+  IonCard,
5
+  IonCardContent,
6
+  IonCardHeader,
7
+  IonCardSubtitle,
8
+  IonCardTitle,
9
+  IonContent,
10
+  IonHeader,
11
+  IonPage,
12
+  IonTitle,
13
+  IonToolbar,
14
+} from "@ionic/react";
15
+import { RouteComponentProps } from "react-router";
16
+
17
+const SupervisadasPage: React.FC = () => {
18
+  return (
19
+    <IonPage>
20
+      <IonHeader>
21
+        <IonToolbar>
22
+          <IonButtons slot="start">
23
+            <IonBackButton />
24
+          </IonButtons>
25
+          <IonTitle>Servicios</IonTitle>
26
+        </IonToolbar>
27
+      </IonHeader>
28
+      <IonContent fullscreen>
29
+        <IonCard>
30
+          <img
31
+            alt="Silhouette of mountains"
32
+            src="https://ionicframework.com/docs/img/demos/card-media.png"
33
+          />
34
+          <IonCardHeader>
35
+            <IonCardTitle>Visitas Supervisadas</IonCardTitle>
36
+            <IonCardSubtitle></IonCardSubtitle>
37
+          </IonCardHeader>
38
+
39
+          <IonCardContent>
40
+            Espacio tranquilo y seguro para visitas supervisadas
41
+          </IonCardContent>
42
+        </IonCard>
43
+      </IonContent>
44
+    </IonPage>
45
+  );
46
+};
47
+
48
+export default SupervisadasPage;

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

@@ -0,0 +1,37 @@
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
+}
20
+
21
+.video {
22
+  position: relative;
23
+  overflow: hidden;
24
+  width: 100%;
25
+  padding-top: 56.25%; /* 16:9 Aspect Ratio (divide 9 by 16 = 0.5625) */
26
+}
27
+
28
+/* Then style the iframe to fit in the container div with full height and width */
29
+.video iframe {
30
+  position: absolute;
31
+  top: 0;
32
+  left: 0;
33
+  bottom: 0;
34
+  right: 0;
35
+  width: 100%;
36
+  height: 100%;
37
+}

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

@@ -76,12 +76,8 @@ http://ionicframework.com/docs/theming/ */
76 76
   --ion-color-light-tint: #f5f6f9;
77 77
 }
78 78
 
79
+/*
79 80
 @media (prefers-color-scheme: dark) {
80
-  /*
81
-   * Dark Colors
82
-   * -------------------------------------------
83
-   */
84
-
85 81
   body {
86 82
     --ion-color-primary: #428cff;
87 83
     --ion-color-primary-rgb: 66,140,255;
@@ -147,11 +143,6 @@ http://ionicframework.com/docs/theming/ */
147 143
     --ion-color-light-tint: #383a3e;
148 144
   }
149 145
 
150
-  /*
151
-   * iOS Dark Theme
152
-   * -------------------------------------------
153
-   */
154
-
155 146
   .ios body {
156 147
     --ion-background-color: #000000;
157 148
     --ion-background-color-rgb: 0,0,0;
@@ -190,12 +181,6 @@ http://ionicframework.com/docs/theming/ */
190 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 184
   .md body {
200 185
     --ion-background-color: #121212;
201 186
     --ion-background-color-rgb: 18,18,18;
@@ -234,3 +219,4 @@ http://ionicframework.com/docs/theming/ */
234 219
     --ion-card-background: #1e1e1e;
235 220
   }
236 221
 }
222
+*/

+ 8
- 4
src/types.ts View File

@@ -1,4 +1,8 @@
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
+}