Quantcast
Channel: Ionic Forum - Latest posts

IonPage doesn't render after transition occasionally (with Ionic React)

$
0
0

Hi.

My name is Fabian, and I have built an Ionic Capacitor React app for both Android and iOS. However, there is an issue that occurs on the iOS version, where the IonPage occasionally fails to render. This issue doesn’t seem to appear on Android.

There are no errors printed to the console when this issue happens, and the url of the failing page is correct, according to the Sarari inspector, so I don’t think it has to do with the router.

Here are some screenshots:

Page transition starting point (user presses the ‘Browse all videos’ button):

What the next page SHOULD look like:

What the next page occassionally looks like, when the error occurs (~10% of the time):

Tapping one of the tab buttons will fix it, and the correct content will then be rendered

Please help me solve this. I’ve tried several things, like: removing everything within the ion-content tags, checking the state at certain points of execution, reworking the sorting, but I am baffled.

Here is the relevant component code that should renders the IonPage:

import {
  IonBackButton,
  IonButtons,
  IonContent,
  IonHeader,
  IonPage,
  IonSearchbar,
  IonTitle,
  IonToolbar,
  useIonViewDidEnter,
  useIonViewWillEnter,
  useIonViewWillLeave,
} from "@ionic/react";
import { useEffect, useMemo, useRef, useState } from "react";
import { useLocation, useParams } from "react-router";
import { Virtuoso } from "react-virtuoso";
import { useAppSelector } from "../../lib/custom-hooks/useAppSelector";
import { useHomeSubRouting } from "../../lib/custom-hooks/useHomeSubRouting";
import { selectAllCpdVideos } from "../../lib/slices/appContentSlice";
import { CPDListLinkOrigin, CPDVideo, FilterListModalType, SortMode } from "../../lib/types";
import { Utils } from "../../lib/utils";
import { selectAppActivity } from "../../lib/slices/appActivitySlice";
import { Keyboard } from "@capacitor/keyboard";
import VideoFilterButton from "../../shared-components/VideoFilterButton";
import VideoFilterModal from "./VideoFilterModal";
import CPDSortModal from "./CPDSortModal";
import VideoCard from "./VideoCard";
import imgDropdownGreen from "../../assets/images/dropdown_green_n.png";

function CPDHubVideoList() {
  console.log("CPDHubVideoList rendering...");

  const homeSubRoute = useHomeSubRouting();
  const location = useLocation();

  const searchInputRef = useRef<HTMLIonSearchbarElement>(null);
  const filtersContainerRef = useRef<HTMLDivElement | null>(null);
  const prevScrollOffset = useRef(0);

  const params = useParams<{ linkOrigin: CPDListLinkOrigin; optionalParam?: string }>(); // TODO optionalParam will be an event id or category
  const decodedOptionalParam = params.optionalParam ? decodeURIComponent(params.optionalParam) : null;

  const appActivity = useAppSelector(selectAppActivity);
  const allVideos = useAppSelector(selectAllCpdVideos);
  const allVideosForCurrentRoute = useMemo(() => filterAllVideosFromLinkOrigin(), [location.pathname]);

  const [filtersContainerOffset, setFiltersContainerOffset] = useState(0);
  const [searchText, setSearchText] = useState("");
  // TODO use generics here instead of selectedCategories, selectedPresenters etc
  const [selectedCategories, setSelectedCategories] = useState<string[]>(
    decodedOptionalParam ? [decodedOptionalParam] : []
  );
  const [selectedPresenters, setSelectedPresenters] = useState<string[]>([]);
  const [selectedDates, setSelectedDates] = useState<string[]>([]);
  const [sortMode, setSortMode] = useState<SortMode>("Last added");
  const [showSortModal, setShowSortModal] = useState(false);
  const [filterListModal, setFilterListModal] = useState<{
    show: boolean;
    type: FilterListModalType;
    allItems: string[];
  }>({
    show: false,
    type: "",
    allItems: [],
  });

  const uniqueCategories = useMemo(() => Utils.getUniqueCategoriesForVideos(allVideos), []);
  const uniquePresenters = useMemo(() => getUniquePresentersForVideos(), []);
  const uniqueDates = useMemo(() => getUniqueDatesForVideos(), []);

  const filteredVideos = filterVideos();

  const pageTitle = useMemo(() => {
    switch (params.linkOrigin) {
      case "bookmarks":
        return "Bookmarked";
      case "history":
        return "History";
      case "recently-added":
        return "Recently added CPD";
      case "top-rated":
        return "Top Rated";
      case "event":
        return decodedOptionalParam;
      default:
        return "";
    }
  }, []);

  const showSearchInput =
    params.linkOrigin === "search" || params.linkOrigin === "browse" || params.linkOrigin === "category";
  const showFilters =
    params.linkOrigin === "search" ||
    params.linkOrigin === "browse" ||
    params.linkOrigin === "top-rated" ||
    params.linkOrigin === "category" ||
    params.linkOrigin === "event";

  useIonViewDidEnter(() => {
    if (params.linkOrigin === "search") {
      searchInputRef.current?.setFocus();
    }
  }, []);

  useIonViewWillEnter(() => {
    if (filtersContainerRef.current) {
      setFiltersContainerOffset(-filtersContainerRef.current.offsetHeight);
    }
  }, []);

  useIonViewWillLeave(() => {
    setFiltersContainerOffset(filtersContainerRef.current?.offsetHeight!);
  }, []);

  function filterAllVideosFromLinkOrigin() {
    switch (params.linkOrigin) {
      case "bookmarks":
        return [...allVideos].filter((vid) => vid.isBookmarked);
      case "history":
        return [...allVideos].filter((vid) => appActivity.map((it) => it.id).includes(vid.id));
      case "recently-added":
        return [...allVideos].sort((a, b) => b.publishedOrder - a.publishedOrder).slice(0, 10);
      case "event":
        return [...allVideos]; // TODO
      case "top-rated":
        return [...allVideos].sort((a, b) => b.likeCount - a.likeCount).slice(0, 10);
      case "category":
        return [...allVideos].filter((vid) => vid.eventCategory === decodedOptionalParam);
      default:
        return [...allVideos];
    }
  }

  function filterVideos() {
    let videosCopy = [...allVideosForCurrentRoute];

    // sort
    if (sortMode === "Last added") {
      videosCopy = sortVideosByLastAdded(videosCopy);
    } else if (sortMode === "Oldest first") {
      videosCopy = sortVideosOldestFirst(videosCopy);
    } else {
      videosCopy = sortVideosAlphabetically(videosCopy);
    }

    videosCopy = applySearch(videosCopy);
    videosCopy = applyCategoryFiltering(videosCopy);
    videosCopy = applyPresenterFiltering(videosCopy);
    videosCopy = applyDatesFiltering(videosCopy);

    return videosCopy;
  }

  function sortVideosByLastAdded(videos: CPDVideo[]) {
    return [...videos].sort((a, b) => b.publishedOrder - a.publishedOrder);
  }

  function sortVideosOldestFirst(videos: CPDVideo[]) {
    return [...videos].sort((a, b) => a.publishedOrder - b.publishedOrder);
  }

  function sortVideosAlphabetically(videos: CPDVideo[]) {
    return [...videos].sort((a, b) => {
      return a.title.localeCompare(b.title);
    });
  }

  function applySearch(videos: CPDVideo[]) {
    const trimmedText = searchText.toLowerCase().trim();
    if (trimmedText === "") {
      return videos;
    }

    const searched = videos.filter((vid) => {
      return (
        vid.title.toLowerCase().includes(trimmedText) ||
        vid.eventType.toLowerCase().includes(trimmedText) ||
        vid.eventCategory.toLowerCase().includes(trimmedText) ||
        vid.shortDescription.toLowerCase().includes(trimmedText) ||
        vid.presenters.join("").toLowerCase().includes(trimmedText)
      );
    });

    return searched;
  }

  function applyCategoryFiltering(videos: CPDVideo[]) {
    if (!selectedCategories.length) {
      return videos;
    }

    return videos.filter((vid) => selectedCategories.includes(vid.eventCategory));
  }

  function applyPresenterFiltering(videos: CPDVideo[]) {
    if (!selectedPresenters.length) {
      return videos;
    }

    return videos.filter((vid) => {
      return vid.presenters.some((presenterOfVideo) => selectedPresenters.includes(presenterOfVideo));
    });
  }

  function applyDatesFiltering(videos: CPDVideo[]) {
    if (!selectedDates.length) {
      return videos;
    }

    return videos.filter((vid) => selectedDates.includes(vid.eventDate));
  }

  // TODO use generics instead?
  function determineSelectedItemsForFilterModal() {
    switch (filterListModal.type) {
      case "Categories":
        return selectedCategories;
      case "Presenters":
        return selectedPresenters;
      case "Dates":
        return selectedDates;
      default:
        return [];
    }
  }

  function clearSelectedItems() {
    switch (filterListModal.type) {
      case "Categories":
        return setSelectedCategories([]);
      case "Presenters":
        return setSelectedPresenters([]);
      case "Dates":
        return setSelectedDates([]);
    }
  }

  function getUniquePresentersForVideos() {
    const uniqueValues = allVideos
      .flatMap((vid) => vid.presenters)
      .filter((item, index, arr) => arr.indexOf(item) === index);
    return uniqueValues.sort((a, b) => a.localeCompare(b)).filter((it) => it !== "");
  }

  // TODO
  function getUniqueDatesForVideos() {
    const uniqueValues = allVideos
      .map((vid) => vid.eventDate)
      .filter((item, index, arr) => arr.indexOf(item) === index);

    return uniqueValues;
  }

  // show filters container if scrolling up, or scroll position is near top (to work well with iOS bounce). otherwise, hide the filters container
  function handleContentScroll(scrollTop: number) {
    if (filteredVideos.length < 4) return; // don't do anything if there's only a couple of videos to show

    var currentScrollPos = scrollTop;
    if (prevScrollOffset.current > currentScrollPos || currentScrollPos < 10) {
      setFiltersContainerOffset(-filtersContainerRef.current?.offsetHeight!);
    } else {
      setFiltersContainerOffset(filtersContainerRef.current?.offsetHeight!);
    }
    prevScrollOffset.current = currentScrollPos;
  }

  return (
    <IonPage>
      <IonHeader className={"ion-no-border"}>
        <IonToolbar className="t-border">
          <IonButtons
            slot="start"
            class="mr-6"
            style={{
              // Ionic docs recommend using ion-searchbar as the only child to ion-toolbar, but we don't want to do that - which causes back buton misalignment.
              // Add these margins so that the back button is vertically aligned to the centre, like other back buttons. This fix should only be implemented when using a search bar.
              marginTop: showSearchInput ? "var(--padding-top)" : 0,
              marginBottom: showSearchInput ? "var(--padding-bottom)" : 0,
            }}
          >
            <IonBackButton defaultHref={homeSubRoute ? "/app/home/cpd-hub" : "/app/cpd-hub"} />
          </IonButtons>
          {/* TODO fix slight shift in the back button when using this searchbar in the toolbar */}
          {showSearchInput ? (
            <IonSearchbar
              id="video-list-searchbar"
              ref={searchInputRef}
              value={searchText}
              placeholder="Search CPD Hub"
              onIonInput={(e) => setSearchText(e.detail.value!)}
              className="font-normal text-black text-17 leading-27"
            />
          ) : (
            <IonTitle>{pageTitle}</IonTitle>
          )}
        </IonToolbar>
        <div className="relative">
          <div
            className="bg-grey-80 w-full absolute"
            style={{
              height: 300, // number doesn't matter much, just make it high so the background is covered
              bottom: filtersContainerOffset,
            }}
          />
          <div
            ref={filtersContainerRef}
            className="absolute w-full"
            style={{ transition: "bottom 0.2s", bottom: filtersContainerOffset }}
          >
            {showFilters && (
              <div className="t-border bg-grey-80 flex t-border p-2 gap-3 overflow-scroll">
                <VideoFilterButton
                  label="Category"
                  handleClick={() => setFilterListModal({ show: true, type: "Categories", allItems: uniqueCategories })}
                  selectedItemsInFilter={selectedCategories.length}
                />
                <VideoFilterButton
                  label="Presenter"
                  handleClick={() => setFilterListModal({ show: true, type: "Presenters", allItems: uniquePresenters })}
                  selectedItemsInFilter={selectedPresenters.length}
                />
                <VideoFilterButton
                  label="Date"
                  handleClick={() => setFilterListModal({ show: true, type: "Dates", allItems: uniqueDates })}
                  selectedItemsInFilter={selectedDates.length}
                />
              </div>
            )}
            <div
              className="flex justify-between items-center p-3 bg-white"
              style={{ borderBottom: "solid 2px var(--grey-30)" }}
            >
              <div className="flex items-center">
                <div className="text-13 font-semibold text-grey-70">Sort by: &nbsp;</div>
                <button onClick={() => setShowSortModal(true)} className="flex items-center">
                  <div className="text-13 font-semibold text-green-30">{sortMode}</div>
                  <img width={24} height={24} src={imgDropdownGreen} style={{ marginTop: "-4px" }} />
                </button>
              </div>
              <div className="text-13 font-semibold text-grey-70">{`${filteredVideos.length} result${
                filteredVideos.length > 1 ? "s" : ""
              }`}</div>
            </div>
          </div>
        </div>
      </IonHeader>
      <IonContent style={{ "--background": "var(--grey-20)" }} onTouchStart={(e) => Keyboard.hide()}>
        <Virtuoso
          className="t-border"
          data={filteredVideos}
          onScroll={(e) => handleContentScroll(e.currentTarget.scrollTop)}
          itemContent={(index, video) => {
            return (
              // applying padding directly to the virtuoso style prop won't work, so add padding for first item here
              <div style={{ paddingTop: index === 0 ? filtersContainerRef.current?.clientHeight! : "" }}>
                <VideoCard video={video} guestVersion={false} searchText={searchText} />
              </div>
            );
          }}
        />
        <CPDSortModal
          showModal={showSortModal}
          handleClose={() => setShowSortModal(false)}
          handleSortChange={(newSortMode) => setSortMode(newSortMode)}
          currentSortMode={sortMode}
        />
        <VideoFilterModal
          show={filterListModal.show}
          type={filterListModal.type}
          allItems={filterListModal.allItems}
          selectedItems={determineSelectedItemsForFilterModal()}
          resultsCount={filteredVideos.length}
          handleReset={() => clearSelectedItems()}
          handleClose={() => setFilterListModal({ show: false, type: "", allItems: [] })}
          handleSelection={(selectedItem) => {
            switch (filterListModal.type) {
              case "Categories":
                if (selectedCategories.includes(selectedItem)) {
                  // remove from cat list
                  const newItems = [...selectedCategories].filter((cat) => cat !== selectedItem);
                  setSelectedCategories(newItems);
                } else {
                  // add item to cat list
                  setSelectedCategories([...selectedCategories, selectedItem]);
                }
                break;
              case "Presenters":
                if (selectedPresenters.includes(selectedItem)) {
                  const newItems = [...selectedPresenters].filter((cat) => cat !== selectedItem);
                  setSelectedPresenters(newItems);
                } else {
                  setSelectedPresenters([...selectedPresenters, selectedItem]);
                }
                break;
              case "Dates":
                if (selectedDates.includes(selectedItem)) {
                  const newItems = [...selectedDates].filter((cat) => cat !== selectedItem);
                  setSelectedDates(newItems);
                } else {
                  setSelectedDates([...selectedDates, selectedItem]);
                }
                break;
              default:
                throw new Error("Filter type not found");
            }
          }}
        />
      </IonContent>
    </IonPage>
  );
}

export default CPDHubVideoList;

Thanks for any help!

Fabian


IonPage doesn't render after transition occasionally (with Ionic React)

$
0
0

Try wrapping this div in your IonHeader in a second IonToolbar. I’ve seen weird transition issues when using an IonToolbar and a sibling element without an IonToolbar.

CosmosDB with ionic 7

$
0
0

Thanks a lot, I agree with the kind of thinking that you wrote. Yes, in the initial scenario, Cosmos DB is only used for reading information, while the writing is done by Odoo.

IonPage doesn't render after transition occasionally (with Ionic React)

How to configure ionic apk to use https

$
0
0

I’m currently utilizing a certificate authority for security purposes.

the server and the api are in the same network (local network)

everything operates smoothly, including browsing on both HTTP and HTTPS. However, I’m encountering a peculiar issue where HTTPS requests get canceled only when accessed from a mobile device, while HTTP works without any problems.

PWAs: A Powerful Part of your Mobile App Strategy, But Not a Standalone Solution

IonPage doesn't render after transition occasionally (with Ionic React)

$
0
0

FYI, here is my package.json file:

{
  "name": "cop-app",
  "private": true,
  "version": "0.0.1",
  "type": "module",
  "scripts": {
    "build": "ionic capacitor build",
    "preview": "vite preview",
    "test.e2e": "cypress run",
    "test.unit": "vitest",
    "lint": "eslint",
    "sync": "ionic capacitor sync",
    "web": "ionic serve",
    "ios": "ionic capacitor run ios",
    "android": "ionic capacitor run android",
    "ios:l": "ionic capacitor run ios -l --external",
    "android:l": "ionic capacitor run android -l --external",
    "adb": "adb reverse tcp:8081 tcp:8081"
  },
  "dependencies": {
    "@capacitor-community/sqlite": "^5.5.2",
    "@capacitor/android": "5.5.0",
    "@capacitor/app": "5.0.6",
    "@capacitor/browser": "^5.1.0",
    "@capacitor/core": "5.5.0",
    "@capacitor/filesystem": "^5.1.4",
    "@capacitor/haptics": "5.0.6",
    "@capacitor/ios": "5.5.0",
    "@capacitor/keyboard": "5.0.6",
    "@capacitor/network": "^5.0.7",
    "@capacitor/preferences": "^5.0.6",
    "@capacitor/push-notifications": "^5.1.1",
    "@capacitor/screen-orientation": "^5.0.7",
    "@capacitor/share": "^5.0.6",
    "@capacitor/status-bar": "5.0.6",
    "@capacitor/toast": "^5.0.7",
    "@ionic/react": "^7.0.0",
    "@ionic/react-router": "^7.0.0",
    "@reduxjs/toolkit": "^2.1.0",
    "@types/react-router": "^5.1.20",
    "@types/react-router-dom": "^5.3.3",
    "@vimeo/player": "^2.20.1",
    "html-entities": "^2.4.0",
    "html-react-parser": "^5.0.6",
    "ionicons": "^7.0.0",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-loading-indicators": "^0.2.3",
    "react-redux": "^9.1.0",
    "react-router": "^5.3.4",
    "react-router-dom": "^5.3.4",
    "react-virtuoso": "^4.7.2",
    "zod": "^3.22.5"
  },
  "devDependencies": {
    "@capacitor/cli": "5.5.0",
    "@eslint/js": "^9.0.0",
    "@testing-library/jest-dom": "^5.16.5",
    "@testing-library/react": "^14.0.0",
    "@testing-library/user-event": "^14.4.3",
    "@types/react": "^18.0.27",
    "@types/react-dom": "^18.0.10",
    "@types/vimeo__player": "^2.18.3",
    "@vitejs/plugin-legacy": "^4.0.2",
    "@vitejs/plugin-react": "^4.0.1",
    "cypress": "^12.7.0",
    "eslint": "^8.57.0",
    "eslint-plugin-react": "^7.34.1",
    "globals": "^15.0.0",
    "jsdom": "^22.1.0",
    "typescript": "^5.1.6",
    "typescript-eslint": "^7.7.0",
    "vite": "^4.3.9",
    "vitest": "^0.32.2"
  },
  "description": ""
}

Cannot get tabindex to work on Android

$
0
0

I have a page that uses tabindex for ion-inputs. It works properly on a web browser, but in iOS the tabindex is completely ignored, and the “Next” key advances to the next input in the html file. I am on ionic angular 6.10.1. How can this be resolved?


iOS - Tap on status bar doesn't scroll to top

$
0
0

If anyone else is coming here, here is Liam’s response from the issue created - bug: iOS - Tap on status bar doesn’t scroll to top · Issue #29376 · ionic-team/ionic-framework · GitHub

This is to be expected in a web browser. The status bar tap enabled by statusTap: true relies on the Capacitor status bar plugin which only functions in a Capacitor application.

In a web browser, iOS handles the status bar scroll to top when scrolling on the body element. Since your Ionic app scrolls on the ion-content element and not body, tapping the status bar will not scroll the ion-content to the top. iOS does not expose any additional APIs for us to control this unfortunately.

Ion-native Datepicker problems with Dark Mode

$
0
0

This is still a problem in 2024.

Copy / Paste IOS

$
0
0

Couldn’t copy the + symbols in phone number with country code eg: +9190909090 when selecting the text for copy it is not automatically selecting the + symbols

Can anyone help me, issue is only in iOS

IonPage doesn't render after transition occasionally (with Ionic React)

$
0
0

I’ve now reduced my component to this, but the bug still occurs:

import { IonBackButton, IonButtons, IonContent, IonHeader, IonPage, IonToolbar } from "@ionic/react";

function CPDHubVideoList() {
  console.log("CPDHubVideoList rendering...");

  return (
    <IonPage>
      <IonHeader>
        <IonToolbar>
          <IonButtons slot="start">
            <IonBackButton defaultHref={"/app/cpd-hub"} />
          </IonButtons>
        </IonToolbar>
      </IonHeader>
      <IonContent></IonContent>
    </IonPage>
  );
}

export default CPDHubVideoList;

This leads me to believe that my issue is elsewhere.

Here is my app router:

import { IonReactRouter } from "@ionic/react-router";
import { IonIcon, IonLabel, IonRippleEffect, IonRouterOutlet, IonTabBar, IonTabButton, IonTabs } from "@ionic/react";
import { Route, Redirect } from "react-router-dom";
import { selectAuth } from "./lib/slices/authSlice";
import { useAppSelector } from "./lib/custom-hooks/useAppSelector";
import { useState } from "react";
import LandingPage from "./landing/LandingPage";
import HomeTab from "./tabs/home/HomeTab";
import FitnessToPractisePage from "./tabs/members/FitnessToPractisePage";
import ParamedicInsightPage from "./tabs/members/ParamedicInsightPage";
import PastPollsPage from "./tabs/home/PastPollsPage";
import BritishParamedicJournalPage from "./tabs/members/BritishParamedicJournalPage";
import HomeNewsTab from "./tabs/home/HomeNewsTab";
import NewsArticlePage from "./tabs/news/NewsArticlePage";
import NewsTab from "./tabs/news/NewsTab";
import CPDHubTab from "./tabs/cpd-hub/CPDHubTab";
import CPDVideoDetailsPage from "./tabs/cpd-hub/CPDVideoDetailsPage";
import EventsTab from "./tabs/events/EventsTab";
import EventDetailsPage from "./tabs/events/EventDetailsPage";
import MembersTabForGuests from "./tabs/members/MembersTabForGuests";
import MembersTab from "./tabs/members/MembersTab";
import ProfileDetailsPage from "./tabs/members/ProfileDetailsPage";
import CommunicationPreferencesPage from "./tabs/members/CommunicationPreferencesPage";
import LiabilityInsurancePage from "./tabs/members/LiabilityInsurancePage";
import ELearningResourcesPage from "./tabs/members/ELearningResourcesPage";
import MentalHealthAndWellbeingAppPage from "./tabs/members/MentalHealthAndWellbeingAppPage";
import DiscountPage from "./tabs/members/DiscountPage";
import CPDHubVideoList from "./tabs/cpd-hub/CPDHubVideoList";
import CategoryListPage from "./tabs/cpd-hub/CategoryListPage";
import HomeCPDHubPage from "./tabs/home/HomeCPDHubPage";
import HelpAndInfo from "./tabs/members/HelpAndInfo";
import AccessibilitySettings from "./tabs/members/AccessibilitySettings";

function AppRouter() {
  const auth = useAppSelector(selectAuth);

  return (
    <IonReactRouter>
      <IonRouterOutlet mode="ios">
        <Route
          exact
          path={"/"}
          render={(props) =>
            auth.user && !auth.isGuest ? <Redirect to={"/app/home"} /> : <Redirect to={"/landing"} />
          }
        />
        <Route exact path={"/landing"} component={LandingPage} />
        <Route path={"/app"} component={Tabs} />
      </IonRouterOutlet>
    </IonReactRouter>
  );
}

function Tabs() {
  const auth = useAppSelector(selectAuth);

  const [activeTab, setActiveTab] = useState<string>("home");

  return (
    <IonTabs onIonTabsDidChange={(e) => setActiveTab(e.detail.tab)}>
      <IonRouterOutlet mode="ios">
        {/* home tab routes */}
        <Route exact path={"/app/home"} component={HomeTab} />
        <Route exact path={"/app/home/benefits-fitness-to-practise"} component={FitnessToPractisePage} />
        <Route exact path={"/app/home/past-polls"} component={PastPollsPage} />
        <Route exact path={"/app/home/benefits-paramedic-insight"} component={ParamedicInsightPage} />
        <Route exact path={"/app/home/benefits-british-paramedic-journal"} component={BritishParamedicJournalPage} />
        {/* - news sub routes in home tab */}
        <Route exact path={"/app/home/news"} component={HomeNewsTab} />
        <Route exact path={"/app/home/news/article-:articleId"} component={NewsArticlePage} />
        {/* - cpd hub sub routes in home tab */}
        <Route exact path={"/app/home/cpd-hub"} component={HomeCPDHubPage} />
        <Route exact path={"/app/home/cpd-hub/categories"} component={CategoryListPage} />
        <Route exact path={"/app/home/cpd-hub/video-list-:linkOrigin/:optionalParam?"} component={CPDHubVideoList} />
        <Route exact path={"/app/home/cpd-hub/video-detail-:videoId"} component={CPDVideoDetailsPage} />
        {/* events sub routes in home tab */}
        <Route exact path={"/app/home/events"} component={EventsTab} />
        <Route exact path={"/app/home/events/event-:eventId"} component={EventDetailsPage} />
        {/* news tab routes */}
        <Route exact path={"/app/news"} component={NewsTab} />
        <Route exact path={"/app/news/article-:articleId"} component={NewsArticlePage} />
        {/* cpd hub tab routes */}
        <Route exact path={"/app/cpd-hub"} component={CPDHubTab} />
        <Route exact path={"/app/cpd-hub/categories"} component={CategoryListPage} />
        <Route exact path={"/app/cpd-hub/video-list-:linkOrigin/:optionalParam?"} component={CPDHubVideoList} />
        <Route exact path={"/app/cpd-hub/video-detail-:videoId"} component={CPDVideoDetailsPage} />
        {/* events tab routes */}
        <Route exact path={"/app/events"} component={EventsTab} />
        <Route exact path={"/app/events/event-:eventId"} component={EventDetailsPage} />
        {/* members tab routes */}
        <Route
          exact
          path={"/app/members"}
          render={(props) => (auth.isGuest ? <MembersTabForGuests /> : <MembersTab />)}
        />
        <Route exact path={"/app/members/profile-details"} component={ProfileDetailsPage} />
        <Route exact path={"/app/members/profile-accessibility-settings"} component={AccessibilitySettings} />
        <Route exact path={"/app/members/profile-communication-preferences"} component={CommunicationPreferencesPage} />
        <Route exact path={"/app/members/profile-help-and-info"} component={HelpAndInfo} />
        <Route exact path={"/app/members/benefits-paramedic-insight"} component={ParamedicInsightPage} />
        <Route exact path={"/app/members/benefits-british-paramedic-journal"} component={BritishParamedicJournalPage} />
        <Route exact path={"/app/members/benefits-fitness-to-practise"} component={FitnessToPractisePage} />
        <Route exact path={"/app/members/benefits-liability-insurance"} component={LiabilityInsurancePage} />
        <Route exact path={"/app/members/benefits-e-learning-resources"} component={ELearningResourcesPage} />
        <Route
          exact
          path={"/app/members/benefits-mental-health-and-wellbeing-app"}
          component={MentalHealthAndWellbeingAppPage}
        />
        <Route exact path={"/app/members/discounts-:discountId"} component={DiscountPage} />
      </IonRouterOutlet>
      <IonTabBar slot="bottom">
        {/* Note that icons are set via CSS */}
        <IonTabButton tab="home" href="/app/home">
          <IonIcon className="tab-icon" />
          <IonLabel>Home</IonLabel>
          <div className="bg-green-30 w-full absolute top-0 h-3 invisible" />
          <IonRippleEffect />
        </IonTabButton>
        <IonTabButton tab="news" href="/app/news">
          <IonIcon className="tab-icon" />
          <IonLabel>News</IonLabel>
          <div className="bg-green-30 w-full absolute top-0 h-3 invisible" />
          <IonRippleEffect />
        </IonTabButton>
        <IonTabButton tab="cpd-hub" href="/app/cpd-hub">
          <IonIcon className="tab-icon" />
          <IonLabel>CPD Hub</IonLabel>
          <div className="bg-green-30 w-full absolute top-0 h-3 invisible" />
          <IonRippleEffect />
        </IonTabButton>
        <IonTabButton tab="events" href="/app/events">
          <IonIcon className="tab-icon" />
          <IonLabel>Events</IonLabel>
          <div className="bg-green-30 w-full absolute top-0 h-3 invisible" />
          <IonRippleEffect />
        </IonTabButton>
        <IonTabButton tab="members" href="/app/members">
          <IonIcon className="tab-icon" />
          <IonLabel>Members</IonLabel>
          <div className="bg-green-30 w-full absolute top-0 h-3 invisible" />
          <IonRippleEffect />
        </IonTabButton>
      </IonTabBar>
    </IonTabs>
  );
}

export default AppRouter;

… and here is the component that has the nav links:

import { IonRippleEffect, useIonRouter } from "@ionic/react";
import imgDisclosureBlue from "../assets/images/disclosure_right_blue.png";
import { useAppSelector } from "../lib/custom-hooks/useAppSelector";
import { useHomeSubRouting } from "../lib/custom-hooks/useHomeSubRouting";
import { selectAllCpdVideos } from "../lib/slices/appContentSlice";
import { selectAuth } from "../lib/slices/authSlice";
import VideoCard from "../tabs/cpd-hub/VideoCard";
import VideoCardMini from "../tabs/cpd-hub/VideoCardMini";
import GuestVideoCard from "./GuestVideoCard";
import HeadingAndButtonContainer from "./HeadingAndButtonContainer";
import SmallIcon from "./SmallIcon";

function CPDHubContent() {
  const homeSubRoute = useHomeSubRouting();

  const nav = useIonRouter();

  const auth = useAppSelector(selectAuth);
  const allVideos = useAppSelector(selectAllCpdVideos);

  const filteredVideos = auth.isGuest ? allVideos.filter((vid) => vid.isSample) : allVideos;
  const recentlyAdded = [...filteredVideos].sort((a, b) => b.publishedOrder - a.publishedOrder).slice(0, 5);
  const topRated = [...filteredVideos].sort((a, b) => b.likeCount - a.likeCount).slice(0, 5);
  const topCategoriesMapping = getTopFiveCategories();

  function getTopFiveCategories() {
    // build object of key-value (category:count) pairs
    const mapping = allVideos.reduce((counts, item) => {
      const { eventCategory } = item;
      counts[eventCategory] = (counts[eventCategory] || 0) + 1;
      return counts;
    }, {} as { [category: string]: number });

    // sort the object based on the counts
    const sorted = Object.entries(mapping).sort(([, a], [, b]) => b - a);

    // get the categories only
    const topCats = sorted.map((it) => it[0]);

    return topCats.slice(0, 5);
  }

  function getRoute(endpoint: string) {
    return homeSubRoute ? `/app/home/cpd-hub/${endpoint}` : `/app/cpd-hub/${endpoint}`;
  }

  return (
    <div className="p-4">
      {auth.isGuest ? (
        <div id="guest-video-content-container" className="">
          <GuestVideoCard />
          {filteredVideos?.map((video) => (
            <div key={video.id} className="border border-solid border-grey-40  mt-4">
              <VideoCard video={video} guestVersion={true} />
            </div>
          ))}
        </div>
      ) : (
        <>
          <CPDHubLink text="Browse all videos" link={getRoute("video-list-browse")} />
          <CPDHubLink text="Bookmarked" link={getRoute("video-list-bookmarks")} />
          <CPDHubLink text="History" link={getRoute("video-list-history")} />
          <div className="mt-5">
            <HeadingAndButtonContainer
              heading="Recently added"
              btnNavPath={getRoute("video-list-recently-added")}
              buttonText="See all"
            />
            <div className="flex t-border overflow-scroll gap-2" style={{ marginLeft: "-16px", marginRight: "-16px" }}>
              {recentlyAdded.map((video, i, arr) => (
                <div
                  key={video.id}
                  style={{ paddingLeft: i === 0 ? "16px" : "", paddingRight: i === arr.length - 1 ? "16px" : "" }}
                >
                  <VideoCardMini
                    video={video}
                    handleClick={() => {
                      const route = getRoute(`video-detail-${video.id}`);
                      nav.push(route);
                    }}
                  />
                </div>
              ))}
            </div>
          </div>
          <div className="mt-5">
            <HeadingAndButtonContainer
              heading="Top rated"
              btnNavPath={getRoute(`video-list-top-rated`)}
              buttonText="See all"
            />
            <div className="flex t-border overflow-scroll gap-2" style={{ marginLeft: "-16px", marginRight: "-16px" }}>
              {topRated.map((video, i, arr) => (
                <div
                  key={video.id}
                  style={{ paddingLeft: i === 0 ? "16px" : "", paddingRight: i === arr.length - 1 ? "16px" : "" }}
                >
                  <VideoCardMini
                    video={video}
                    handleClick={() =>
                      nav.push(
                        homeSubRoute
                          ? `/app/home/cpd-hub/video-detail-${video.id}`
                          : `/app/cpd-hub/video-detail-${video.id}`
                      )
                    }
                  />
                </div>
              ))}
            </div>
          </div>
          <div className="mt-5">
            <HeadingAndButtonContainer
              heading="Popular Categories"
              btnNavPath={getRoute("categories")}
              buttonText="See all"
            />
            {topCategoriesMapping.map((cat) => {
              // encode category because it could contain slashes (e.g. the category 'Obsterics/Maternity') and other unwanted characters
              return (
                <CPDHubLink key={cat} text={cat} link={getRoute(`video-list-category/${encodeURIComponent(cat)}`)} />
              );
            })}
          </div>
        </>
      )}
    </div>
  );
}

interface CPDHubLinkProps {
  text: string;
  link: string;
}

function CPDHubLink(props: CPDHubLinkProps) {
  const nav = useIonRouter();

  return (
    <button
      className="flex items-center t-border w-full py-1 ion-activatable relative"
      onClick={() => nav.push(props.link)}
      style={{ borderBottom: "solid 2px var(--grey-30)" }}
    >
      <SmallIcon text={props.text} width={24} height={24} />
      <div className="text-20 font-semibold leading-28 flex-1 t-border text-left pl-2">{props.text}</div>
      <img src={imgDisclosureBlue} className="t-border" width={44} height={44} />
      <IonRippleEffect />
    </button>
  );
}

export default CPDHubContent;

How to use native api to upload images from gallery and camera?

$
0
0

A question about GalleryImageOptions.limit: "Maximum number of pictures the user will be able to choose. Note: This option is only supported on Android 13+ and iOS. ". But Android 13 was just released in 2022, how did previous systems use limit. Or are there any other community plugins of recommended

How do Capacitor build Android's debug Signature apk

$
0
0

Use cap build, you can choose AAB or APK, but not debug or release Signature

Invalid keystore password when adding android certificate

$
0
0

I’m trying to add a certificate for Android in Appflow. I’ve uploaded my keystore file and filled in the credentials but it keeps telling me “Invalid keystore password. (keystore file password)”. I’m pretty sure I’ve filled in the correct passwords so not sure why it’s giving this error. Checked the network tab to see what data is being send on submit and the credentials are correct (so no typos).

One thing that’s not really clear is that it asks for certificate password. I assume this is the keystore password but maybe it’s something else?


Ionic V5 android app camera not working with android SDK 33

$
0
0

Hello,

I have created a Android app with using the IONIC version 5 Cordova plugin and used Android version 9

when I uploaded the build on the Google Play Store, it required setting up Android SDK 33 so we made changes accordingly in the config xml file.

Now the build is uploaded in Google Console but our camera functionality is not working in that build, The normal APK camera works fine on Android devices. for the camera, we have used Cordova-plugin-camera plugins.

so any help would be greatly appreciated.

Thanks,
Vipul Jadvani

Ion-native Datepicker problems with Dark Mode

$
0
0

So, are you looking for help? A lot has changed since 2020. Ionic Native is now Awesome Cordova Plugins.

There is no mention of the plugin being used. Either way, all Ionic Native plugins are just wrappers around Cordova plugins. If there is a problem with the plugin, it would be best to open an issue on the respective repo. All Cordova plugins are third-party plugins and have nothing to do with Ionic.

Also since 2020, Ionic has completely re-written their own datetime picker.

Ionic V5 android app camera not working with android SDK 33

$
0
0

Are you using Cordova or Capacitor? You usually cannot just update the config and expect things to work with newer SDKs. You usually have to update Cordova/Capacitor as well.

Ionic V5 android app camera not working with android SDK 33

$
0
0

Since they mention “android version 9” I guess that’s cordova-android 9.

You should update to cordova-android 12 and cordova-plugin-camera 7.0.0

How do Capacitor build Android's debug Signature apk

$
0
0

build command only builds release builds, debug builds are not supported at the moment.

If you use run command a debug build is created for running.

Ionic V5 android app camera not working with android SDK 33

$
0
0

I’ve tried updating android version, but while creatin release build some how it’s keep changing android version to 9





Latest Images