Repository: Couchers-org/couchers (language: python)

Issue:
Create all events page
This PR represents the entire feature branch.
**Context:**
User clicks on the events page, can see it's events, the discover section but wants to see it's events, so clicks the "see all" button

**Page behavior:**

- tab “upcoming”.
all events created by the user or to be participated (and that have not started yet), cancelled or not

- tab "past":
all events created by the user or that he/she has participated in but that have already happened, cancelled or not

![image](https://github.com/user-attachments/assets/7a606a5a-61bf-4043-986e-28353e5fb423)



New Events Page - tests and move folders
This is the final PR for the events feature. This PR adds some tests, and moves the new events files into the regular events folder, deletes the old duplicates (like `EventsPage`) and renames a few components still being used in communities (`EventsList` => `CommunityEventsList`), etc.

**NOTE:** This will merge into the feature branch, not `develop`.

**Web frontend checklist**
- [ x ] Formatted my code with `yarn format`
- [ x ] There are no warnings from `yarn lint --fix`
- [ x ] There are no console warnings when running the app (well, no new ones...)
- [ ] Added any new components to storybook
- [ x ] Added tests where relevant
- [ x ] All tests pass
- [ x ] Clicked around my changes running locally and it works
- [ x ] Checked Desktop, Mobile and Tablet screen sizes



Discussion / hints:
note: 
same event component as here https://github.com/Couchers-org/couchers/issues/4784 ignore the UI difference, it's just due the software I'm using to create this mockups


Fix patch:
diff --git a/app/web/components/LocationAutocomplete/LocationAutocomplete.tsx b/app/web/components/LocationAutocomplete/LocationAutocomplete.tsx
index 109b283958..3b26f47acf 100644
--- a/app/web/components/LocationAutocomplete/LocationAutocomplete.tsx
+++ b/app/web/components/LocationAutocomplete/LocationAutocomplete.tsx
@@ -9,6 +9,7 @@ import { Control, useController } from "react-hook-form";
 import { GeocodeResult, useGeocodeQuery } from "utils/hooks";
 
 interface LocationAutocompleteProps {
+  className?: string;
   control: Control;
   defaultValue: GeocodeResult | "";
   fieldError: string | undefined;
@@ -25,6 +26,7 @@ interface LocationAutocompleteProps {
 }
 
 export default function LocationAutocomplete({
+  className,
   control,
   defaultValue,
   fieldError,
@@ -100,6 +102,7 @@ export default function LocationAutocomplete({
 
   return (
     <Autocomplete
+      className={className}
       id={id}
       innerRef={controller.field.ref}
       label={label}
diff --git a/app/web/components/Pill/Pill.tsx b/app/web/components/Pill/Pill.tsx
index 77159d5015..b1c1d58a61 100644
--- a/app/web/components/Pill/Pill.tsx
+++ b/app/web/components/Pill/Pill.tsx
@@ -2,12 +2,21 @@ import { Typography } from "@material-ui/core";
 import classNames from "classnames";
 import makeStyles from "utils/makeStyles";
 
+interface PillStylesProps {
+  backgroundColor?: string;
+  color?: string;
+}
+
 const useStyles = makeStyles((theme) => ({
   root: {
-    backgroundColor: theme.palette.grey[200],
+    backgroundColor: (props: PillStylesProps) =>
+      props.backgroundColor ?? theme.palette.grey[200],
+    color: (props: PillStylesProps) =>
+      props.color ?? theme.palette.text.primary,
     padding: theme.spacing(0.5, 1),
     textAlign: "center",
     fontWeight: "bold",
+    margin: theme.spacing(0.5),
   },
   rounded: {
     borderRadius: theme.shape.borderRadius * 6,
@@ -16,11 +25,18 @@ const useStyles = makeStyles((theme) => ({
 
 export interface PillProps {
   children: React.ReactNode;
+  backgroundColor?: string;
+  color?: string;
   variant?: "rounded" | "square";
 }
 
-export default function Pill({ children, variant = "rounded" }: PillProps) {
-  const classes = useStyles();
+export default function Pill({
+  children,
+  backgroundColor,
+  color,
+  variant = "rounded",
+}: PillProps) {
+  const classes = useStyles({ backgroundColor, color });
 
   return (
     <Typography
diff --git a/app/web/features/communities/CommunityPage/CommunityPage.tsx b/app/web/features/communities/CommunityPage/CommunityPage.tsx
index ae0cdc95db..e51abfdef4 100644
--- a/app/web/features/communities/CommunityPage/CommunityPage.tsx
+++ b/app/web/features/communities/CommunityPage/CommunityPage.tsx
@@ -9,7 +9,7 @@ import makeStyles from "utils/makeStyles";
 import CommunityBase from "../CommunityBase";
 import CommunityInfoPage from "../CommunityInfoPage";
 import { DiscussionsListPage, DiscussionsSection } from "../discussions";
-import EventsList from "../events/EventsList";
+import CommunityEventsList from "../events/CommunityEventsList";
 import EventsSection from "../events/EventsSection";
 import PageHeader from "../PageHeader";
 import CommunityPageSubHeader from "./CommunityPageSubHeader";
@@ -101,7 +101,7 @@ export default function CommunityPage({
             ) : tab === "discussions" ? (
               <DiscussionsListPage community={community} />
             ) : tab === "events" ? (
-              <EventsList community={community} />
+              <CommunityEventsList community={community} />
             ) : null}
           </>
         );
diff --git a/app/web/features/communities/events/EventsList.stories.tsx b/app/web/features/communities/events/CommunityEventsList.stories.tsx
similarity index 82%
rename from app/web/features/communities/events/EventsList.stories.tsx
rename to app/web/features/communities/events/CommunityEventsList.stories.tsx
index f1bb0fb875..2d222a8b8e 100644
--- a/app/web/features/communities/events/EventsList.stories.tsx
+++ b/app/web/features/communities/events/CommunityEventsList.stories.tsx
@@ -3,20 +3,20 @@ import { mockedService } from "stories/serviceMocks";
 import community from "test/fixtures/community.json";
 import events from "test/fixtures/events.json";
 
-import EventsList from "./EventsList";
+import CommunityEventsList from "./CommunityEventsList";
 
 export default {
-  component: EventsList,
-  title: "Communities/Events/EventsList",
+  component: CommunityEventsList,
+  title: "Communities/Events/CommunityEventsList",
 } as Meta;
 
-interface EventsListArgs {
+interface CommunityEventsListArgs {
   pagesOfEvent?: "none" | "one-page" | "multiple-pages";
   shouldListAttendeesSucceed?: boolean;
   shouldListEventsSucceed?: boolean;
 }
 
-const Template: Story<EventsListArgs> = ({
+const Template: Story<CommunityEventsListArgs> = ({
   pagesOfEvent = "one-page",
   shouldListAttendeesSucceed = true,
   shouldListEventsSucceed = true,
@@ -26,7 +26,7 @@ const Template: Story<EventsListArgs> = ({
     shouldListAttendeesSucceed,
     shouldListEventsSucceed,
   });
-  return <EventsList community={community} />;
+  return <CommunityEventsList community={community} />;
 };
 
 export const OnePageOfEvents = Template.bind({});
@@ -49,7 +49,7 @@ ErrorLoadingEvents.args = {
 function setMocks({
   pagesOfEvent,
   shouldListEventsSucceed,
-}: Required<EventsListArgs>) {
+}: Required<CommunityEventsListArgs>) {
   mockedService.events.listCommunityEvents = async () => {
     return shouldListEventsSucceed
       ? {
diff --git a/app/web/features/communities/events/CommunityEventsList.tsx b/app/web/features/communities/events/CommunityEventsList.tsx
new file mode 100644
index 0000000000..c5e2f66b0b
--- /dev/null
+++ b/app/web/features/communities/events/CommunityEventsList.tsx
@@ -0,0 +1,86 @@
+import { CircularProgress } from "@material-ui/core";
+import Alert from "components/Alert";
+import Button from "components/Button";
+import { CalendarIcon } from "components/Icons";
+import TextBody from "components/TextBody";
+import { useTranslation } from "i18n";
+import { COMMUNITIES } from "i18n/namespaces";
+import { useRouter } from "next/router";
+import { Community } from "proto/communities_pb";
+import { routeToNewEvent } from "routes";
+import hasAtLeastOnePage from "utils/hasAtLeastOnePage";
+import makeStyles from "utils/makeStyles";
+
+import { SectionTitle, useCommunityPageStyles } from "../CommunityPage";
+import { useListCommunityEvents } from "../hooks";
+import CommunityLongEventCard from "./CommunityLongEventCard";
+
+interface CommunityEventsListProps {
+  community: Community.AsObject;
+}
+
+const useStyles = makeStyles((theme) => ({
+  eventsListContainer: {
+    display: "grid",
+    rowGap: theme.spacing(3),
+    [theme.breakpoints.down("xs")]: {
+      //break out of page padding
+      left: "50%",
+      marginLeft: "-50vw",
+      marginRight: "-50vw",
+      position: "relative",
+      right: "50%",
+      width: "100vw",
+    },
+    marginBlockEnd: theme.spacing(2),
+  },
+}));
+
+export default function CommunityEventsList({
+  community,
+}: CommunityEventsListProps) {
+  const { t } = useTranslation([COMMUNITIES]);
+  const classes = { ...useCommunityPageStyles(), ...useStyles() };
+  const router = useRouter();
+
+  const { data, error, hasNextPage, fetchNextPage, isLoading } =
+    useListCommunityEvents({
+      communityId: community.communityId,
+      pageSize: 5,
+      type: "all",
+    });
+
+  return (
+    <>
+      <SectionTitle icon={<CalendarIcon />}>
+        {t("communities:events_title")}
+      </SectionTitle>
+      <Button
+        className={classes.createResourceButton}
+        onClick={() => router.push(routeToNewEvent(community.communityId))}
+      >
+        {t("communities:create_an_event")}
+      </Button>
+      {error && <Alert severity="error">{error.message}</Alert>}
+      <div className={classes.eventsListContainer}>
+        {isLoading ? (
+          <CircularProgress />
+        ) : hasAtLeastOnePage(data, "eventsList") ? (
+          data.pages
+            .flatMap((page) => page.eventsList)
+            .filter((event) => !event.isCancelled)
+            .map((event) => (
+              <CommunityLongEventCard event={event} key={event.eventId} />
+            ))
+        ) : (
+          !error && <TextBody>{t("communities:events_empty_state")}</TextBody>
+        )}
+      </div>
+      {hasNextPage && (
+        <Button onClick={() => fetchNextPage()}>
+          {t("communities:see_more_events_label")}
+        </Button>
+      )}
+    </>
+  );
+}
diff --git a/app/web/features/communities/events/CommunityLongEventCard.stories.tsx b/app/web/features/communities/events/CommunityLongEventCard.stories.tsx
new file mode 100644
index 0000000000..5b2889f249
--- /dev/null
+++ b/app/web/features/communities/events/CommunityLongEventCard.stories.tsx
@@ -0,0 +1,49 @@
+import { Meta, Story } from "@storybook/react";
+import { mockedService } from "stories/serviceMocks";
+import events from "test/fixtures/events.json";
+
+import CommunityLongEventCard, {
+  CommunityLongEventCardProps,
+} from "./CommunityLongEventCard";
+
+export default {
+  component: CommunityLongEventCard,
+  title: "Communities/Events/CommunityLongEventCard",
+} as Meta;
+
+interface CommunityLongEventCardArgs extends CommunityLongEventCardProps {
+  returnFullPage?: boolean;
+}
+
+const Template: Story<CommunityLongEventCardArgs> = ({
+  event,
+  returnFullPage = true,
+}) => {
+  setMocks(returnFullPage);
+  return <CommunityLongEventCard event={event} />;
+};
+
+export const EventWithLotsOfAttendees = Template.bind({});
+EventWithLotsOfAttendees.args = {
+  event: events[0],
+};
+
+export const OnlineEvent = Template.bind({});
+OnlineEvent.args = {
+  event: events[1],
+};
+
+export const EventWithFewAttendees = Template.bind({});
+EventWithFewAttendees.args = {
+  event: events[2],
+  returnFullPage: false,
+};
+
+function setMocks(returnFullPage: boolean) {
+  mockedService.events.listEventAttendees = async () => {
+    return {
+      nextPageToken: "",
+      attendeeUserIdsList: returnFullPage ? [1, 2, 3, 4, 5] : [1, 2, 3],
+    };
+  };
+}
diff --git a/app/web/features/communities/events/CommunityLongEventCard.tsx b/app/web/features/communities/events/CommunityLongEventCard.tsx
new file mode 100644
index 0000000000..ed76561d41
--- /dev/null
+++ b/app/web/features/communities/events/CommunityLongEventCard.tsx
@@ -0,0 +1,147 @@
+import {
+  Card,
+  Theme,
+  Typography,
+  useMediaQuery,
+  useTheme,
+} from "@material-ui/core";
+import { eventImagePlaceholderUrl } from "appConstants";
+import { AttendeesIcon, CalendarIcon } from "components/Icons";
+import { useTranslation } from "i18n";
+import { COMMUNITIES } from "i18n/namespaces";
+import Link from "next/link";
+import { Event } from "proto/events_pb";
+import { useMemo } from "react";
+import { routeToEvent } from "routes";
+import { timestamp2Date } from "utils/date";
+import dayjs from "utils/dayjs";
+import makeStyles from "utils/makeStyles";
+
+import getContentSummary from "../getContentSummary";
+
+const useStyles = makeStyles<Theme, { eventImageSrc: string }>((theme) => ({
+  overviewRoot: {
+    "&:hover": {
+      backgroundColor: theme.palette.grey[50],
+    },
+    padding: theme.spacing(1),
+    [theme.breakpoints.up("sm")]: {
+      padding: theme.spacing(2),
+    },
+  },
+  overviewContent: {
+    display: "grid",
+    gap: theme.spacing(1),
+    gridTemplateAreas: `
+        "titles image"
+        "content content"
+      `,
+    gridTemplateColumns: "1fr 1fr",
+    [theme.breakpoints.up("md")]: {
+      gridTemplateColumns: "2fr 1fr",
+    },
+  },
+  eventInfoContainer: {
+    gridArea: "titles",
+  },
+  eventTimeContainer: {
+    alignItems: "center",
+    display: "flex",
+    marginBlockStart: theme.spacing(1),
+  },
+  attendeesCountContainer: {
+    alignItems: "center",
+    display: "flex",
+  },
+  icon: {
+    display: "block",
+    fontSize: "1.25rem",
+    lineHeight: 1.5,
+    marginInlineEnd: theme.spacing(0.5),
+  },
+  image: {
+    objectFit: ({ eventImageSrc }) =>
+      eventImageSrc === eventImagePlaceholderUrl ? "contain" : "cover",
+    height: 80,
+    [theme.breakpoints.up("md")]: {
+      height: 120,
+    },
+    width: "100%",
+    gridArea: "image",
+  },
+  onlineOrOfflineInfo: {
+    fontWeight: "bold",
+    color: theme.palette.grey[600],
+  },
+  content: {
+    gridArea: "content",
+  },
+}));
+
+export interface CommunityLongEventCardProps {
+  event: Event.AsObject;
+}
+
+export default function CommunityLongEventCard({
+  event,
+}: CommunityLongEventCardProps) {
+  const { t } = useTranslation([COMMUNITIES]);
+  const classes = useStyles({
+    eventImageSrc: event.photoUrl || eventImagePlaceholderUrl,
+  });
+  const theme = useTheme();
+  const isBelowLg = useMediaQuery(theme.breakpoints.down("md"));
+
+  const truncatedContent = useMemo(
+    () =>
+      getContentSummary({
+        originalContent: event.content,
+        maxLength: isBelowLg ? 120 : 300,
+      }),
+    [event.content, isBelowLg]
+  );
+  const startTime = dayjs(timestamp2Date(event.startTime!));
+
+  return (
+    <Card className={classes.overviewRoot}>
+      <Link href={routeToEvent(event.eventId, event.slug)}>
+        <a className={classes.overviewContent}>
+          <div className={classes.eventInfoContainer}>
+            <Typography variant="h2">{event.title}</Typography>
+            <Typography
+              className={classes.onlineOrOfflineInfo}
+              color="primary"
+              variant="body1"
+            >
+              {event.offlineInformation
+                ? event.offlineInformation.address
+                : t("communities:online")}
+            </Typography>
+            <div className={classes.eventTimeContainer}>
+              <CalendarIcon className={classes.icon} />
+              <Typography variant="body1">
+                {startTime.format("ll LT")}
+              </Typography>
+            </div>
+            <div className={classes.attendeesCountContainer}>
+              <AttendeesIcon className={classes.icon} />
+              <Typography variant="body1">
+                {t("communities:attendees_count", {
+                  count: event.goingCount + event.maybeCount,
+                })}
+              </Typography>
+            </div>
+          </div>
+          <Typography className={classes.content} variant="body1">
+            {truncatedContent}
+          </Typography>
+          <img
+            alt=""
+            className={classes.image}
+            src={event.photoUrl || eventImagePlaceholderUrl}
+          />
+        </a>
+      </Link>
+    </Card>
+  );
+}
diff --git a/app/web/features/communities/events/DiscoverEventsList.tsx b/app/web/features/communities/events/DiscoverEventsList.tsx
new file mode 100644
index 0000000000..b1f253cbf4
--- /dev/null
+++ b/app/web/features/communities/events/DiscoverEventsList.tsx
@@ -0,0 +1,208 @@
+import {
+  CircularProgress,
+  Typography,
+  useMediaQuery,
+  useTheme,
+} from "@material-ui/core";
+import { Pagination } from "@material-ui/lab";
+import Alert from "components/Alert";
+import LocationAutocomplete from "components/LocationAutocomplete";
+import TextBody from "components/TextBody";
+import { useTranslation } from "i18n";
+import { COMMUNITIES, GLOBAL } from "i18n/namespaces";
+import { useState } from "react";
+import { useForm } from "react-hook-form";
+import { GeocodeResult } from "utils/hooks";
+import makeStyles from "utils/makeStyles";
+
+import EventsList from "./EventsList";
+import { useEventSearch } from "./hooks";
+
+const useStyles = makeStyles((theme) => ({
+  column: {
+    display: "flex",
+    flexDirection: "column",
+  },
+  emptyState: {
+    marginBottom: theme.spacing(2),
+  },
+  filter: {
+    backgroundColor: theme.palette.grey[200],
+    color: theme.palette.text.primary,
+    padding: theme.spacing(1, 2),
+    textAlign: "center",
+    fontWeight: "bold",
+    margin: theme.spacing(0.5),
+    borderRadius: theme.shape.borderRadius * 6,
+    "&:hover": {
+      cursor: "pointer",
+    },
+  },
+  filterTags: {
+    display: "flex",
+    alignItems: "center",
+    marginTop: theme.spacing(2),
+    marginBottom: theme.spacing(2),
+  },
+  loadingBox: {
+    display: "flex",
+    justifyContent: "center",
+    padding: theme.spacing(2),
+    width: "100%",
+    minHeight: theme.spacing(20),
+  },
+  locationSearch: {
+    marginRight: theme.spacing(2),
+    paddingBottom: theme.spacing(2),
+  },
+  pagination: {
+    display: "flex",
+    justifyContent: "center",
+    marginTop: theme.spacing(2),
+    marginBottom: theme.spacing(2),
+  },
+  row: {
+    display: "flex",
+    justifyContent: "space-between",
+    alignItems: "center",
+    width: "100%",
+    [theme.breakpoints.down("sm")]: {
+      flexDirection: "column",
+    },
+  },
+  selectedFilter: {
+    backgroundColor: theme.palette.secondary.main,
+    color: theme.palette.common.white,
+    padding: theme.spacing(1, 2),
+    textAlign: "center",
+    fontWeight: "bold",
+    margin: theme.spacing(0.5),
+    borderRadius: theme.shape.borderRadius * 6,
+    "&:hover": {
+      cursor: "pointer",
+    },
+  },
+}));
+
+const DiscoverEventsList = () => {
+  const classes = useStyles();
+  const { control, errors } = useForm({
+    mode: "onChange",
+  });
+  const { t } = useTranslation([GLOBAL, COMMUNITIES]);
+  const theme = useTheme();
+  const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
+  const pageSize = 12;
+
+  const [pageNumber, setPageNumber] = useState(1);
+  const [isMyCommunities, setIsMyCommunities] = useState<boolean>(false);
+  const [isOnlineOnly, setIsOnlineOnly] = useState<boolean>(false);
+  const [locationResult, setLocationResult] = useState<GeocodeResult | "">("");
+
+  const { data, error, isLoading } = useEventSearch({
+    pastEvents: false,
+    pageSize,
+    pageNumber,
+    isMyCommunities,
+    isOnlineOnly,
+    searchLocation: locationResult,
+  });
+
+  const hasEvents = data && data.eventsList && data.eventsList.length > 0;
+  const numPages = Math.ceil((data?.totalItems ?? 0) / pageSize) ?? 1;
+
+  const handlePageNumberChange = (
+    event: React.ChangeEvent<unknown>,
+    value: number
+  ) => {
+    setPageNumber(value);
+  };
+
+  const handleFilterIsMyCommunitiesClick = () => {
+    setIsMyCommunities(!isMyCommunities);
+    setPageNumber(1);
+  };
+
+  const handleFilterIsOnlineOnlyClick = () => {
+    setIsOnlineOnly(!isOnlineOnly);
+    setPageNumber(1);
+  };
+
+  const handleOnChangeAutocomplete = (newLocationResult: GeocodeResult) => {
+    if (typeof newLocationResult === "object") {
+      setLocationResult(newLocationResult);
+    } else {
+      setLocationResult("");
+    }
+    setPageNumber(1);
+  };
+
+  const renderLocationAutoComplete = () => (
+    <LocationAutocomplete
+      className={classes.locationSearch}
+      control={control}
+      name="location"
+      defaultValue={typeof locationResult === "object" ? locationResult : ""}
+      label={t("global:location_autocomplete.search_location_button")}
+      onChange={handleOnChangeAutocomplete}
+      fieldError={errors.location?.message}
+      fullWidth={isMobile}
+    />
+  );
+
+  return (
+    <>
+      <div className={classes.column}>
+        <Typography variant="h2">
+          {t("communities:discover_events_title")}
+        </Typography>
+        <div className={classes.row}>
+          <div className={classes.filterTags}>
+            <Typography
+              className={
+                isMyCommunities ? classes.selectedFilter : classes.filter
+              }
+              variant="body2"
+              onClick={handleFilterIsMyCommunitiesClick}
+            >
+              {t("communities:my_communities")}
+            </Typography>
+            <Typography
+              className={isOnlineOnly ? classes.selectedFilter : classes.filter}
+              variant="body2"
+              onClick={handleFilterIsOnlineOnlyClick}
+            >
+              {t("communities:online")}
+            </Typography>
+          </div>
+          {!isMobile && renderLocationAutoComplete()}
+        </div>
+        {isMobile && renderLocationAutoComplete()}
+      </div>
+      {!hasEvents && !isLoading && (
+        <TextBody>{t("communities:events_empty_state")}</TextBody>
+      )}
+      {error && <Alert severity="error">{error.message}</Alert>}
+      {isLoading && (
+        <div className={classes.loadingBox}>
+          <CircularProgress />
+        </div>
+      )}
+      {hasEvents && !isLoading && (
+        <>
+          <EventsList events={data.eventsList} isVerticalStyle />
+          <Pagination
+            className={classes.pagination}
+            count={numPages}
+            page={pageNumber}
+            color="primary"
+            onChange={handlePageNumberChange}
+            size="large"
+          />
+        </>
+      )}
+    </>
+  );
+};
+
+export default DiscoverEventsList;
diff --git a/app/web/features/communities/events/EventsList.tsx b/app/web/features/communities/events/EventsList.tsx
index 315278cfd7..f94ea24ab0 100644
--- a/app/web/features/communities/events/EventsList.tsx
+++ b/app/web/features/communities/events/EventsList.tsx
@@ -1,82 +1,82 @@
-import { CircularProgress } from "@material-ui/core";
-import Alert from "components/Alert";
-import Button from "components/Button";
-import { CalendarIcon } from "components/Icons";
-import TextBody from "components/TextBody";
-import { useTranslation } from "i18n";
-import { COMMUNITIES } from "i18n/namespaces";
-import { useRouter } from "next/router";
-import { Community } from "proto/communities_pb";
-import { routeToNewEvent } from "routes";
-import hasAtLeastOnePage from "utils/hasAtLeastOnePage";
+import { useAuthContext } from "features/auth/AuthProvider";
+import { Event } from "proto/events_pb";
 import makeStyles from "utils/makeStyles";
 
-import { SectionTitle, useCommunityPageStyles } from "../CommunityPage";
-import { useListCommunityEvents } from "../hooks";
+import EventCard from "./EventCard";
 import LongEventCard from "./LongEventCard";
 
-interface EventsListProps {
-  community: Community.AsObject;
+interface EventListProps {
+  events: Event.AsObject[];
+  isVerticalStyle?: boolean;
 }
 
 const useStyles = makeStyles((theme) => ({
-  eventsListContainer: {
-    display: "grid",
-    rowGap: theme.spacing(3),
-    [theme.breakpoints.down("xs")]: {
-      //break out of page padding
-      left: "50%",
-      marginLeft: "-50vw",
-      marginRight: "-50vw",
-      position: "relative",
-      right: "50%",
-      width: "100vw",
+  root: {
+    display: "flex",
+    flexDirection: "column",
+  },
+  eventsContainer: (props: { isVerticalStyle: boolean }) =>
+    props.isVerticalStyle
+      ? {
+          display: "grid",
+
+          [theme.breakpoints.down("xs")]: {
+            gridTemplateColumns: "1fr",
+            gridGap: theme.spacing(2),
+            //break out of page padding
+            left: "50%",
+            marginLeft: "-50vw",
+            marginRight: "-50vw",
+            position: "relative",
+            right: "50%",
+            width: "100vw",
+          },
+          [theme.breakpoints.up("sm")]: {
+            display: "grid",
+            gridTemplateColumns: "repeat(2, 1fr)",
+            gridGap: theme.spacing(2),
+          },
+          [theme.breakpoints.up("md")]: {
+            gridTemplateColumns: "repeat(3, 1fr)",
+            gridGap: theme.spacing(3),
+          },
+        }
+      : {},
+  seeMoreContainer: {
+    display: "flex",
+    justifyContent: "center",
+    marginTop: theme.spacing(4),
+    [theme.breakpoints.down("sm")]: {
+      marginBottom: theme.spacing(4),
     },
-    marginBlockEnd: theme.spacing(2),
   },
 }));
 
-export default function EventsList({ community }: EventsListProps) {
-  const { t } = useTranslation([COMMUNITIES]);
-  const classes = { ...useCommunityPageStyles(), ...useStyles() };
-  const router = useRouter();
+const DEFAULT_EVENTS: Event.AsObject[] = [];
+
+const EventsList = ({
+  events = DEFAULT_EVENTS,
+  isVerticalStyle = false,
+}: EventListProps) => {
+  const classes = useStyles({ isVerticalStyle });
 
-  const { data, error, hasNextPage, fetchNextPage, isLoading } =
-    useListCommunityEvents({
-      communityId: community.communityId,
-      pageSize: 5,
-      type: "all",
-    });
+  const {
+    authState: { userId },
+  } = useAuthContext();
 
   return (
-    <>
-      <SectionTitle icon={<CalendarIcon />}>
-        {t("communities:events_title")}
-      </SectionTitle>
-      <Button
-        className={classes.createResourceButton}
-        onClick={() => router.push(routeToNewEvent(community.communityId))}
-      >
-        {t("communities:create_an_event")}
-      </Button>
-      {error && <Alert severity="error">{error.message}</Alert>}
-      <div className={classes.eventsListContainer}>
-        {isLoading ? (
-          <CircularProgress />
-        ) : hasAtLeastOnePage(data, "eventsList") ? (
-          data.pages
-            .flatMap((page) => page.eventsList)
-            .filter((event) => !event.isCancelled)
-            .map((event) => <LongEventCard event={event} key={event.eventId} />)
-        ) : (
-          !error && <TextBody>{t("communities:events_empty_state")}</TextBody>
+    <div className={classes.root}>
+      <div className={classes.eventsContainer}>
+        {events.map((event) =>
+          isVerticalStyle ? (
+            <EventCard key={event.eventId} event={event} />
+          ) : (
+            <LongEventCard key={event.eventId} event={event} userId={userId} />
+          )
         )}
       </div>
-      {hasNextPage && (
-        <Button onClick={() => fetchNextPage()}>
-          {t("communities:see_more_events_label")}
-        </Button>
-      )}
-    </>
+    </div>
   );
-}
+};
+
+export default EventsList;
diff --git a/app/web/features/communities/events/EventsPage.stories.tsx b/app/web/features/communities/events/EventsPage.stories.tsx
index 2c2d2ae252..0474db9fe1 100644
--- a/app/web/features/communities/events/EventsPage.stories.tsx
+++ b/app/web/features/communities/events/EventsPage.stories.tsx
@@ -1,6 +1,4 @@
 import { Meta, Story } from "@storybook/react";
-import { mockedService } from "stories/serviceMocks";
-import events from "test/fixtures/events.json";
 
 import EventsPage from "./EventsPage";
 
@@ -9,49 +7,7 @@ export default {
   title: "Communities/Events/EventsPage",
 } as Meta;
 
-interface EventsListArgs {
-  pagesOfEvent?: "none" | "one-page" | "multiple-pages";
-  shouldListEventsSucceed?: boolean;
-}
+const Template: Story<typeof EventsPage> = () => <EventsPage />;
 
-const Template: Story<EventsListArgs> = ({
-  pagesOfEvent = "one-page",
-  shouldListEventsSucceed = true,
-} = {}) => {
-  setMocks({
-    pagesOfEvent,
-    shouldListEventsSucceed,
-  });
-  return <EventsPage />;
-};
-
-export const OnePageOfEvents = Template.bind({});
-
-export const MultiplePagesOfEvents = Template.bind({});
-MultiplePagesOfEvents.args = {
-  pagesOfEvent: "multiple-pages",
-};
-
-export const NoEvents = Template.bind({});
-NoEvents.args = {
-  pagesOfEvent: "none",
-};
-
-export const ErrorLoadingEvents = Template.bind({});
-ErrorLoadingEvents.args = {
-  shouldListEventsSucceed: false,
-};
-
-function setMocks({
-  pagesOfEvent,
-  shouldListEventsSucceed,
-}: Required<EventsListArgs>) {
-  mockedService.events.listAllEvents = async () => {
-    return shouldListEventsSucceed
-      ? {
-          eventsList: pagesOfEvent === "none" ? [] : events,
-          nextPageToken: pagesOfEvent === "multiple-pages" ? "3" : "",
-        }
-      : Promise.reject(new Error("Error listing community events"));
-  };
-}
+export const Default = Template.bind({});
+Default.args = {};
diff --git a/app/web/features/communities/events/EventsPage.tsx b/app/web/features/communities/events/EventsPage.tsx
index b739bfe75f..e5108cb82b 100644
--- a/app/web/features/communities/events/EventsPage.tsx
+++ b/app/web/features/communities/events/EventsPage.tsx
@@ -1,44 +1,62 @@
-import { Typography } from "@material-ui/core";
-import { TabContext, TabPanel } from "@material-ui/lab";
-import HtmlMeta from "components/HtmlMeta";
+import { Button, Typography } from "@material-ui/core";
 import PageTitle from "components/PageTitle";
-import TabBar from "components/TabBar";
-import { EventsType } from "features/queryKeys";
 import { useTranslation } from "i18n";
 import { COMMUNITIES, GLOBAL } from "i18n/namespaces";
-import { useState } from "react";
+import { useRouter } from "next/router";
+import { newEventRoute } from "routes";
+import makeStyles from "utils/makeStyles";
 
-import EventsTab from "./EventsTab";
+import DiscoverEventsList from "../events/DiscoverEventsList";
+import MyEventsList from "./MyEventsList";
 
-export default function EventsPage() {
-  const { t } = useTranslation([GLOBAL, COMMUNITIES]);
-  const [tab, setTab] = useState<EventsType>("upcoming");
+const useStyles = makeStyles((theme) => ({
+  button: {
+    backgroundColor: theme.palette.primary.main,
+    color: theme.palette.common.white,
+    margin: theme.spacing(2),
+    padding: theme.spacing(1, 2),
+    "&:hover": {
+      backgroundColor: theme.palette.primary.dark,
+    },
+    fontWeight: "bold",
+  },
+  column: {
+    display: "flex",
+    flexDirection: "column",
+  },
+  headerRow: {
+    display: "flex",
+    justifyContent: "space-between",
+    width: "100%",
+    paddingBottom: theme.spacing(1),
+  },
+}));
+
+const EventsPage = () => {
+  const classes = useStyles();
+  const router = useRouter();
 
-  const allEventsPageTabLabels: Record<EventsType, string> = {
-    upcoming: t("communities:upcoming"),
-    past: t("communities:past"),
-  };
+  const { t } = useTranslation([GLOBAL, COMMUNITIES]);
 
   return (
-    <>
-      <HtmlMeta title={t("global:nav.events")} />
-      <PageTitle>{t("communities:discover_events_title")}</PageTitle>
-      <Typography variant="body1">
-        {t("communities:discover_events_subtitle")}
-      </Typography>
-      <TabContext value={tab}>
-        <TabBar
-          ariaLabel={t("communities:all_events_page_tabs_a11y_label")}
-          setValue={setTab}
-          labels={allEventsPageTabLabels}
-        />
-        <TabPanel value="upcoming">
-          <EventsTab tabTitle={t("communities:upcoming")} />
-        </TabPanel>
-        <TabPanel value="past">
-          <EventsTab pastEvents tabTitle={t("communities:past")} />
-        </TabPanel>
-      </TabContext>
-    </>
+    <div>
+      <div className={classes.headerRow}>
+        <PageTitle>{t("communities:events_title")}</PageTitle>
+        <Button
+          className={classes.button}
+          size="small"
+          onClick={() => router.push(newEventRoute)}
+        >
+          {t("communities:create_new_event")}
+        </Button>
+      </div>
+      <div className={classes.column}>
+        <Typography variant="h2">{t("communities:your_events")}</Typography>
+        <MyEventsList />
+      </div>
+      <DiscoverEventsList />
+    </div>
   );
-}
+};
+
+export default EventsPage;
diff --git a/app/web/features/communities/events/LongEventCard.stories.tsx b/app/web/features/communities/events/LongEventCard.stories.tsx
index 9753d9705a..6f959af16d 100644
--- a/app/web/features/communities/events/LongEventCard.stories.tsx
+++ b/app/web/features/communities/events/LongEventCard.stories.tsx
@@ -1,47 +1,42 @@
+import { ThemeProvider } from "@material-ui/core/styles";
 import { Meta, Story } from "@storybook/react";
-import { mockedService } from "stories/serviceMocks";
-import events from "test/fixtures/events.json";
+import { Event } from "proto/events_pb";
+import React from "react";
+import mockEvents from "test/fixtures/events.json";
+import { theme } from "theme";
 
-import LongEventCard, { LongEventCardProps } from "./LongEventCard";
+import LongEventCard from "./LongEventCard";
 
 export default {
-  component: LongEventCard,
   title: "Communities/Events/LongEventCard",
+  component: LongEventCard,
+  decorators: [
+    (Story) => (
+      <ThemeProvider theme={theme}>
+        <Story />
+      </ThemeProvider>
+    ),
+  ],
 } as Meta;
 
-interface LongEventCardArgs extends LongEventCardProps {
-  returnFullPage?: boolean;
-}
+const Template: Story<{ event: Event.AsObject; userId: number | null }> = (
+  args
+) => <LongEventCard {...args} />;
 
-const Template: Story<LongEventCardArgs> = ({
-  event,
-  returnFullPage = true,
-}) => {
-  setMocks(returnFullPage);
-  return <LongEventCard event={event} />;
-};
-
-export const EventWithLotsOfAttendees = Template.bind({});
-EventWithLotsOfAttendees.args = {
-  event: events[0],
+export const DefaultLongEventCard = Template.bind({});
+DefaultLongEventCard.args = {
+  event: mockEvents[0],
+  userId: 123,
 };
 
 export const OnlineEvent = Template.bind({});
 OnlineEvent.args = {
-  event: events[1],
+  event: mockEvents[1],
+  userId: 123,
 };
 
-export const EventWithFewAttendees = Template.bind({});
-EventWithFewAttendees.args = {
-  event: events[2],
-  returnFullPage: false,
+export const CancelledEvent = Template.bind({});
+CancelledEvent.args = {
+  event: mockEvents[3],
+  userId: 123,
 };
-
-function setMocks(returnFullPage: boolean) {
-  mockedService.events.listEventAttendees = async () => {
-    return {
-      nextPageToken: "",
-      attendeeUserIdsList: returnFullPage ? [1, 2, 3, 4, 5] : [1, 2, 3],
-    };
-  };
-}
diff --git a/app/web/features/communities/events/LongEventCard.tsx b/app/web/features/communities/events/LongEventCard.tsx
index 35df42b8ac..53b8f4244b 100644
--- a/app/web/features/communities/events/LongEventCard.tsx
+++ b/app/web/features/communities/events/LongEventCard.tsx
@@ -1,145 +1,159 @@
 import {
   Card,
+  CardContent,
+  CardMedia,
   Theme,
+  Tooltip,
   Typography,
-  useMediaQuery,
-  useTheme,
 } from "@material-ui/core";
 import { eventImagePlaceholderUrl } from "appConstants";
-import { AttendeesIcon, CalendarIcon } from "components/Icons";
+import Pill from "components/Pill";
 import { useTranslation } from "i18n";
 import { COMMUNITIES } from "i18n/namespaces";
 import Link from "next/link";
 import { Event } from "proto/events_pb";
-import { useMemo } from "react";
 import { routeToEvent } from "routes";
+import { theme } from "theme";
 import { timestamp2Date } from "utils/date";
 import dayjs from "utils/dayjs";
 import makeStyles from "utils/makeStyles";
 
-import getContentSummary from "../getContentSummary";
-
-const useStyles = makeStyles<Theme, { eventImageSrc: string }>((theme) => ({
-  overviewRoot: {
-    "&:hover": {
-      backgroundColor: theme.palette.grey[50],
-    },
-    padding: theme.spacing(1),
-    [theme.breakpoints.up("sm")]: {
-      padding: theme.spacing(2),
+const useStyles = makeStyles((theme: Theme) => ({
+  root: {
+    margin: 0,
+    "&:not(:first-child)": {
+      margin: theme.spacing(2, 0),
     },
+    border: `1px solid ${theme.palette.grey[300]}`,
+    borderRadius: theme.spacing(1),
   },
-  overviewContent: {
-    display: "grid",
-    gap: theme.spacing(1),
-    gridTemplateAreas: `
-      "titles image"
-      "content content"
-    `,
-    gridTemplateColumns: "1fr 1fr",
-    [theme.breakpoints.up("md")]: {
-      gridTemplateColumns: "2fr 1fr",
+  attendees: {
+    display: "flex",
+    alignItems: "flex-end",
+    justifyContent: "flex-end",
+    minWidth: theme.spacing(10),
+    fontSize: ".85rem",
+    color: theme.palette.text.secondary,
+  },
+  card: {
+    display: "flex",
+    width: "100%",
+    height: theme.spacing(20),
+    [theme.breakpoints.down("xs")]: {
+      height: "auto",
     },
   },
-  eventInfoContainer: {
-    gridArea: "titles",
+  cardMedia: {
+    height: "100%",
+    width: "25%",
+    objectFit: "fill",
   },
-  eventTimeContainer: {
-    alignItems: "center",
+  cardContent: {
+    width: "75%",
     display: "flex",
-    marginBlockStart: theme.spacing(1),
+    flexDirection: "column",
+    justifyContent: "space-between",
   },
-  attendeesCountContainer: {
-    alignItems: "center",
+  eventInfo: {
     display: "flex",
+    justifyContent: "flex-end",
+    flexDirection: "column",
+    fontSize: ".85rem",
   },
-  icon: {
-    display: "block",
-    fontSize: "1.25rem",
-    lineHeight: 1.5,
-    marginInlineEnd: theme.spacing(0.5),
+  row: {
+    display: "flex",
+    justifyContent: "space-between",
   },
-  image: {
-    objectFit: ({ eventImageSrc }) =>
-      eventImageSrc === eventImagePlaceholderUrl ? "contain" : "cover",
-    height: 80,
-    [theme.breakpoints.up("md")]: {
-      height: 120,
+  tags: {
+    minWidth: theme.spacing(15),
+    [theme.breakpoints.down("xs")]: {
+      minWidth: theme.spacing(10),
     },
-    width: "100%",
-    gridArea: "image",
-  },
-  onlineOrOfflineInfo: {
-    fontWeight: "bold",
-    color: theme.palette.grey[600],
   },
-  content: {
-    gridArea: "content",
+  title: {
+    display: "-webkit-box",
+    lineClamp: 2,
+    boxOrient: "vertical",
+    overflow: "hidden",
+    textOverflow: "ellipsis",
+    maxHeight: "3em" /* Approximate height for 2 lines of text */,
+    lineHeight: "1.5em",
+    paddingRight: theme.spacing(2),
   },
 }));
 
-export interface LongEventCardProps {
+const LongEventCard = ({
+  event,
+  userId,
+}: {
   event: Event.AsObject;
-}
-
-export default function LongEventCard({ event }: LongEventCardProps) {
-  const { t } = useTranslation([COMMUNITIES]);
+  userId: number | null | undefined;
+}) => {
   const classes = useStyles({
     eventImageSrc: event.photoUrl || eventImagePlaceholderUrl,
   });
-  const theme = useTheme();
-  const isBelowLg = useMediaQuery(theme.breakpoints.down("md"));
+  const { t } = useTranslation([COMMUNITIES]);
 
-  const truncatedContent = useMemo(
-    () =>
-      getContentSummary({
-        originalContent: event.content,
-        maxLength: isBelowLg ? 120 : 300,
-      }),
-    [event.content, isBelowLg]
-  );
-  const startTime = dayjs(timestamp2Date(event.startTime!));
+  const startTime = dayjs(timestamp2Date(event.startTime!)).format("llll");
+  const isCreatedByMe = event.creatorUserId === userId;
+  const isOnline = event.onlineInformation?.link !== undefined;
+  const isCancelled = event.isCancelled;
 
   return (
-    <Card className={classes.overviewRoot}>
+    <Card className={classes.root} data-testid="event-item">
       <Link href={routeToEvent(event.eventId, event.slug)}>
-        <a className={classes.overviewContent}>
-          <div className={classes.eventInfoContainer}>
-            <Typography variant="h2">{event.title}</Typography>
-            <Typography
-              className={classes.onlineOrOfflineInfo}
-              color="primary"
-              variant="body1"
-            >
-              {event.offlineInformation
-                ? event.offlineInformation.address
-                : t("communities:online")}
-            </Typography>
-            <div className={classes.eventTimeContainer}>
-              <CalendarIcon className={classes.icon} />
-              <Typography variant="body1">
-                {startTime.format("ll LT")}
-              </Typography>
+        <a className={classes.card}>
+          <CardMedia
+            className={classes.cardMedia}
+            component="img"
+            image={event.photoUrl || eventImagePlaceholderUrl}
+          />
+          <CardContent className={classes.cardContent}>
+            <div className={classes.row}>
+              <Tooltip title={event.title}>
+                <Typography variant="h3" className={classes.title}>
+                  {event.title}
+                </Typography>
+              </Tooltip>
+              <div className={classes.tags}>
+                {isCreatedByMe && (
+                  <Pill variant="rounded">
+                    {t("communities:created_by_me")}
+                  </Pill>
+                )}
+                {isOnline && (
+                  <Pill variant="rounded">{t("communities:online")}</Pill>
+                )}
+                {isCancelled && (
+                  <Pill
+                    backgroundColor={theme.palette.error.main}
+                    color={theme.palette.common.white}
+                    variant="rounded"
+                  >
+                    {t("communities:cancelled")}
+                  </Pill>
+                )}
+              </div>
             </div>
-            <div className={classes.attendeesCountContainer}>
-              <AttendeesIcon className={classes.icon} />
-              <Typography variant="body1">
+            <div className={classes.row}>
+              <div className={classes.eventInfo}>
+                {event.offlineInformation
+                  ? event.offlineInformation.address
+                  : t("communities:virtual_event_location_placeholder")}
+
+                <div>{startTime}</div>
+              </div>
+              <div className={classes.attendees}>
                 {t("communities:attendees_count", {
                   count: event.goingCount + event.maybeCount,
                 })}
-              </Typography>
+              </div>
             </div>
-          </div>
-          <Typography className={classes.content} variant="body1">
-            {truncatedContent}
-          </Typography>
-          <img
-            alt=""
-            className={classes.image}
-            src={event.photoUrl || eventImagePlaceholderUrl}
-          />
+          </CardContent>
         </a>
       </Link>
     </Card>
   );
-}
+};
+
+export default LongEventCard;
diff --git a/app/web/features/communities/events/MyEventsList.tsx b/app/web/features/communities/events/MyEventsList.tsx
new file mode 100644
index 0000000000..2d98ccfa27
--- /dev/null
+++ b/app/web/features/communities/events/MyEventsList.tsx
@@ -0,0 +1,151 @@
+import { CircularProgress, Typography } from "@material-ui/core";
+import { Pagination } from "@material-ui/lab";
+import Alert from "components/Alert";
+import TextBody from "components/TextBody";
+import { EventsType } from "features/queryKeys";
+import { useTranslation } from "i18n";
+import { COMMUNITIES } from "i18n/namespaces";
+import { useState } from "react";
+import makeStyles from "utils/makeStyles";
+
+import EventsList from "./EventsList";
+import { useListMyEvents } from "./hooks";
+
+const useStyles = makeStyles((theme) => ({
+  emptyState: {
+    marginBottom: theme.spacing(2),
+  },
+  filter: {
+    backgroundColor: theme.palette.grey[200],
+    color: theme.palette.text.primary,
+    padding: theme.spacing(1, 2),
+    textAlign: "center",
+    fontWeight: "bold",
+    margin: theme.spacing(0.5),
+    borderRadius: theme.shape.borderRadius * 6,
+    "&:hover": {
+      cursor: "pointer",
+    },
+  },
+  filterTags: {
+    display: "flex",
+    alignItems: "center",
+    marginTop: theme.spacing(2),
+    marginBottom: theme.spacing(2),
+  },
+  loadingBox: {
+    display: "flex",
+    justifyContent: "center",
+    padding: theme.spacing(2),
+    width: "100%",
+    minHeight: theme.spacing(20),
+  },
+  pagination: {
+    display: "flex",
+    justifyContent: "center",
+    marginTop: theme.spacing(2),
+    marginBottom: theme.spacing(2),
+  },
+  selectedFilter: {
+    backgroundColor: theme.palette.secondary.main,
+    color: theme.palette.common.white,
+    padding: theme.spacing(1, 2),
+    textAlign: "center",
+    fontWeight: "bold",
+    margin: theme.spacing(0.5),
+    borderRadius: theme.shape.borderRadius * 6,
+    "&:hover": {
+      cursor: "pointer",
+    },
+  },
+}));
+
+const MyEventsList = () => {
+  const classes = useStyles();
+  const { t } = useTranslation([COMMUNITIES]);
+  const pageSize = 5;
+
+  const [pageNumber, setPageNumber] = useState(1);
+  const [eventType, setEventType] = useState<EventsType>("upcoming");
+  const [showCancelled, setShowCancelled] = useState<boolean>(false);
+
+  const { data, error, isLoading } = useListMyEvents({
+    pastEvents: eventType === "past",
+    pageSize,
+    pageNumber,
+    showCancelled,
+  });
+
+  const hasEvents = data && data.eventsList && data.eventsList.length > 0;
+  const numPages = Math.ceil((data?.totalItems ?? 0) / pageSize) ?? 1;
+
+  const handlePageNumberChange = (
+    event: React.ChangeEvent<unknown>,
+    value: number
+  ) => {
+    setPageNumber(value);
+  };
+
+  const handleFilterPastClick = () => {
+    if (eventType === "upcoming") {
+      setEventType("past");
+    } else {
+      setEventType("upcoming");
+    }
+    setPageNumber(1);
+  };
+
+  const handleFilterShowCancelledClick = () => {
+    setShowCancelled(!showCancelled);
+    setPageNumber(1);
+  };
+
+  return (
+    <>
+      <div className={classes.filterTags}>
+        <Typography
+          className={
+            eventType === "past" ? classes.selectedFilter : classes.filter
+          }
+          variant="body2"
+          onClick={handleFilterPastClick}
+        >
+          {t("communities:past")}
+        </Typography>
+        <Typography
+          className={showCancelled ? classes.selectedFilter : classes.filter}
+          variant="body2"
+          onClick={handleFilterShowCancelledClick}
+        >
+          {t("communities:show_cancelled_events")}
+        </Typography>
+      </div>
+      {!hasEvents && !isLoading && (
+        <TextBody className={classes.emptyState}>
+          {t("communities:events_empty_state")}
+        </TextBody>
+      )}
+      {error && <Alert severity="error">{error.message}</Alert>}
+      {isLoading && (
+        <div className={classes.loadingBox}>
+          <CircularProgress />
+        </div>
+      )}
+      {hasEvents && !isLoading && (
+        <>
+          <EventsList events={data.eventsList} />
+          <Pagination
+            className={classes.pagination}
+            count={numPages}
+            page={pageNumber}
+            color="primary"
+            onChange={handlePageNumberChange}
+            size="large"
+          />
+        </>
+      )}
+    </>
+  );
+};
+
+export default MyEventsList;
diff --git a/app/web/features/communities/events/hooks.ts b/app/web/features/communities/events/hooks.ts
index af90916c19..a078561143 100644
--- a/app/web/features/communities/events/hooks.ts
+++ b/app/web/features/communities/events/hooks.ts
@@ -3,6 +3,7 @@ import {
   eventKey,
   eventOrganizersKey,
   eventsKey,
+  myEventsKey,
   QueryType,
 } from "features/queryKeys";
 import useUsers from "features/userQueries/useUsers";
@@ -12,10 +13,13 @@ import {
   ListAllEventsRes,
   ListEventAttendeesRes,
   ListEventOrganizersRes,
+  ListMyEventsRes,
 } from "proto/events_pb";
+import { EventSearchRes } from "proto/search_pb";
 import { useInfiniteQuery, useQuery } from "react-query";
 import { service } from "service";
-import type { ListAllEventsInput } from "service/events";
+import type { ListAllEventsInput, ListMyEventsInput } from "service/events";
+import { GeocodeResult } from "utils/hooks";
 
 export interface UseEventUsersInput {
   eventId: number;
@@ -124,3 +128,62 @@ export function useListAllEvents({
     getNextPageParam: (lastPage) => lastPage.nextPageToken || undefined,
   });
 }
+
+export function useListMyEvents({
+  pastEvents,
+  pageNumber,
+  pageSize,
+  showCancelled,
+}: Omit<ListMyEventsInput, "pageToken">) {
+  return useQuery<ListMyEventsRes.AsObject, RpcError>({
+    queryKey: [
+      myEventsKey(pastEvents ? "past" : "upcoming"),
+      pageNumber,
+      showCancelled,
+    ],
+    queryFn: ({ pageParam }) =>
+      service.events.listMyEvents({
+        pastEvents,
+        pageNumber,
+        pageSize,
+        pageToken: pageParam,
+        showCancelled,
+      }),
+  });
+}
+
+export function useEventSearch({
+  pageNumber,
+  pageSize,
+  pastEvents,
+  isMyCommunities,
+  isOnlineOnly,
+  searchLocation,
+}: {
+  pageNumber: number;
+  pageSize: number;
+  pastEvents?: boolean;
+  isMyCommunities?: boolean;
+  isOnlineOnly?: boolean;
+  searchLocation?: GeocodeResult | "";
+}) {
+  return useQuery<EventSearchRes.AsObject, RpcError>({
+    queryKey: [
+      "searchEvents",
+      isMyCommunities,
+      isOnlineOnly,
+      pageNumber,
+      pastEvents,
+      searchLocation,
+    ],
+    queryFn: () =>
+      service.search.EventSearch({
+        pageNumber,
+        pageSize,
+        pastEvents,
+        isMyCommunities,
+        isOnlineOnly,
+        searchLocation,
+      }),
+  });
+}
diff --git a/app/web/features/communities/locales/en.json b/app/web/features/communities/locales/en.json
index 98d5e152f4..2025e280fb 100644
--- a/app/web/features/communities/locales/en.json
+++ b/app/web/features/communities/locales/en.json
@@ -1,125 +1,129 @@
 {
-    "attendees_count": "{{count}} attendee",
-    "attendees_count_plural": "{{count}} attendees",
-    "by_creator": "By {{name}}",
-    "cancelled": "cancelled",
-    "cancel_event": "Cancel event",
-    "show_cancelled_events": "Show cancelled events",
-    "cancel_event_success_message": "The event has been successfully cancelled.",
-    "cancel_event_dialog": {
-        "title": "Cancel event?",
-        "message": "Are you sure you want to cancel this event?"
-    },
-    "invite_community_dialog": {
-        "title": "Request a community invite for this event",
-        "message": "Use this form to request that the moderators send a notification about this event to all nearby users.",
-        "toast_success": "Thanks, the request has been submitted",
-        "link": "More information on this feature"
-    },
-    "invite_community_dialog_buttons": {
-        "open": "Invite the community",
-        "confirm": "Confirm",
-        "close": "Cancel"
-    },
-    "comment": "Comment",
-    "comments": "Comments",
-    "comments_count": "Comments | {{count}}",
-    "community_header": "Welcome to {{name}}",
-    "community_image_input_alt": "Community image input",
-    "upload_helper_text_replace": "Click to change photo",
-    "community_moderators": "Community Builders",
-    "edit_info_page_success_message": "The community page has been successfully updated.",
-    "community_tabs_a11y_label": "Tabs for community sub-pages",
-    "created_at": "Created at ",
-    "create_new_discussion_title": "Create new post",
-    "discussions_empty_state": "No discussions at the moment.",
-    "discussions_label": "Discussions",
-    "discussions_title": "Discussions",
-    "edit_event": "Edit event",
-    "edit_info_page_title": "Edit local info",
-    "error_loading_community": "Error loading the community.",
-    "events_empty_state": "No events at the moment.",
-    "find_host": "Find host",
-    "finished": "Finished",
-    "community_info_page_title": "General information",
-    "hangouts_label": "Hangouts",
-    "invalid_community_id": "Invalid community id.",
-    "join_community": "Join community",
-    "leave_community": "Leave community",
-    "load_earlier_comments": "Load earlier comments",
-    "load_earlier_replies": "Load earlier replies",
-    "load_more_moderators": "Load more Community Builders",
-    "local_info_label": "Local info",
-    "local_info_link": "Go to local info",
-    "local_info_title": "About {{name}}",
-    "more_replies": "More replies...",
-    "more_tips": "More tips and information",
-    "new_discussion_title": "Discussion title",
-    "new_discussion_topic": "Discussion topic",
-    "new_discussion_topic_placeholder": "Create a new discussion topic - e.g. My favourite Amsterdam moment is when...",
-    "new_post_label": "New post",
-    "no_comments": "There are no comments in this discussion at the moment.",
-    "no_moderators": "There aren't any Community Builders here at the moment.",
-    "online": "Online",
-    "overview_label": "Overview",
-    "page_content_field_label": "Page content",
-    "page_content_required": "Please fill the page content",
-    "places_empty_state": "No places to show yet.",
-    "places_title": "Places",
-    "post": "Post",
-    "previous_page": "Go back to previous page",
-    "see_all_moderators": "See all Community Builders",
-    "see_more_discussions_label": "See more discussions",
-    "see_more_information": "See more information",
-    "see_more_places_label": "See more places",
-    "unknown_user": "Unknown user",
-    "upload_image": "Upload image",
-    "details_subheading_colon": "Details:",
-    "details_subheading": "Details",
-    "write_comment_a11y_label": "Write a comment",
-    "attendees": "Attendees",
-    "create_an_event": "Create an event",
-    "create_event_page_title": "Create event",
-    "create_event_disclaimer": "By selecting this button, your event will be publicly displayed for the Couchers.org community to see.",
-    "date_required": "Please enter a date.",
-    "end_date": "End date",
-    "end_time": "End time",
-    "end_time_error": "The event end time must be after the start time.",
-    "event_details": "Event details",
-    "event_details_required": "Please fill in the event details field.",
-    "event_discussion": "Event discussion",
-    "event_image_input_alt": "Event image input",
-    "event_link": "Event link",
-    "events_label": "Events",
-    "events_title": "Events",
-    "invalid_time": "Time must be in 24-hour format HH:MM.",
-    "join_event": "Join event",
-    "leave_event": "Leave event",
-    "going_to_event": "I'm going",
-    "not_going_to_event": "Not going",
-    "link_required": "Please provide a meeting link for the event.",
-    "load_more_attendees": "Load more attendees",
-    "load_more_organizers": "Load more organizers",
-    "location": "Location",
-    "location_required": "Please provide a location for the event.",
-    "no_attendees": "There isn't anyone attending this event yet!",
-    "no_organizers": "There are no organizers for the event.",
-    "organizers": "Organizers",
-    "past_date_error": "The given date must be the same as today or in the future.",
-    "past_time_error": "The given time must be in the future.",
-    "see_all": "See all",
-    "see_more_events_label": "See more events",
-    "start_date": "Start date",
-    "start_time": "Start time",
-    "title_required": "Please provide a title for the event.",
-    "upload_helper_text": "Click to begin uploading an image",
-    "virtual_event": "Virtual event",
-    "virtual_event_link": "Event link",
-    "virtual_event_location_placeholder": "View details for link",
-    "virtual_events_subtext": "Virtual events will be added to the global community.",
-    "past": "Past",
-    "upcoming": "Upcoming",
-    "all_events_page_tabs_a11y_label": "Tabs for the events listing page",
-    "discover_events_subtitle": "Check out upcoming and future events hosted by Couchers.org community members!",
-    "discover_events_title": "Discover Couchers.org events"
+  "attendees_count": "{{count}} attendee",
+  "attendees_count_plural": "{{count}} attendees",
+  "by_creator": "By {{name}}",
+  "cancelled": "Cancelled",
+  "cancel_event": "Cancel event",
+  "show_cancelled_events": "Show cancelled events",
+  "cancel_event_success_message": "The event has been successfully cancelled.",
+  "cancel_event_dialog": {
+    "title": "Cancel event?",
+    "message": "Are you sure you want to cancel this event?"
+  },
+  "invite_community_dialog": {
+    "title": "Request a community invite for this event",
+    "message": "Use this form to request that the moderators send a notification about this event to all nearby users.",
+    "toast_success": "Thanks, the request has been submitted",
+    "link": "More information on this feature"
+  },
+  "invite_community_dialog_buttons": {
+    "open": "Invite the community",
+    "confirm": "Confirm",
+    "close": "Cancel"
+  },
+  "comment": "Comment",
+  "comments": "Comments",
+  "comments_count": "Comments | {{count}}",
+  "community_header": "Welcome to {{name}}",
+  "community_image_input_alt": "Community image input",
+  "upload_helper_text_replace": "Click to change photo",
+  "community_moderators": "Community Builders",
+  "edit_info_page_success_message": "The community page has been successfully updated.",
+  "community_tabs_a11y_label": "Tabs for community sub-pages",
+  "created_at": "Created at ",
+  "create_new_discussion_title": "Create new post",
+  "discussions_empty_state": "No discussions at the moment.",
+  "discussions_label": "Discussions",
+  "discussions_title": "Discussions",
+  "edit_event": "Edit event",
+  "edit_info_page_title": "Edit local info",
+  "error_loading_community": "Error loading the community.",
+  "events_empty_state": "No events at the moment.",
+  "find_host": "Find host",
+  "finished": "Finished",
+  "community_info_page_title": "General information",
+  "hangouts_label": "Hangouts",
+  "invalid_community_id": "Invalid community id.",
+  "join_community": "Join community",
+  "leave_community": "Leave community",
+  "load_earlier_comments": "Load earlier comments",
+  "load_earlier_replies": "Load earlier replies",
+  "load_more_moderators": "Load more Community Builders",
+  "local_info_label": "Local info",
+  "local_info_link": "Go to local info",
+  "local_info_title": "About {{name}}",
+  "more_replies": "More replies...",
+  "more_tips": "More tips and information",
+  "new_discussion_title": "Discussion title",
+  "new_discussion_topic": "Discussion topic",
+  "new_discussion_topic_placeholder": "Create a new discussion topic - e.g. My favourite Amsterdam moment is when...",
+  "new_post_label": "New post",
+  "no_comments": "There are no comments in this discussion at the moment.",
+  "no_moderators": "There aren't any Community Builders here at the moment.",
+  "online": "Online",
+  "my_communities": "My Communities",
+  "overview_label": "Overview",
+  "page_content_field_label": "Page content",
+  "page_content_required": "Please fill the page content",
+  "places_empty_state": "No places to show yet.",
+  "places_title": "Places",
+  "post": "Post",
+  "previous_page": "Go back to previous page",
+  "see_all_moderators": "See all Community Builders",
+  "see_more_discussions_label": "See more discussions",
+  "see_more_information": "See more information",
+  "see_more_places_label": "See more places",
+  "unknown_user": "Unknown user",
+  "upload_image": "Upload image",
+  "details_subheading_colon": "Details:",
+  "details_subheading": "Details",
+  "write_comment_a11y_label": "Write a comment",
+  "attendees": "Attendees",
+  "create_an_event": "Create an event",
+  "create_new_event": "Create New Event",
+  "create_event_page_title": "Create event",
+  "create_event_disclaimer": "By selecting this button, your event will be publicly displayed for the Couchers.org community to see.",
+  "date_required": "Please enter a date.",
+  "end_date": "End date",
+  "end_time": "End time",
+  "end_time_error": "The event end time must be after the start time.",
+  "event_details": "Event details",
+  "event_details_required": "Please fill in the event details field.",
+  "event_discussion": "Event discussion",
+  "event_image_input_alt": "Event image input",
+  "event_link": "Event link",
+  "events_label": "Events",
+  "events_title": "Events",
+  "invalid_time": "Time must be in 24-hour format HH:MM.",
+  "join_event": "Join event",
+  "leave_event": "Leave event",
+  "going_to_event": "I'm going",
+  "not_going_to_event": "Not going",
+  "link_required": "Please provide a meeting link for the event.",
+  "load_more_attendees": "Load more attendees",
+  "load_more_organizers": "Load more organizers",
+  "location": "Location",
+  "location_required": "Please provide a location for the event.",
+  "no_attendees": "There isn't anyone attending this event yet!",
+  "no_organizers": "There are no organizers for the event.",
+  "organizers": "Organizers",
+  "past_date_error": "The given date must be the same as today or in the future.",
+  "past_time_error": "The given time must be in the future.",
+  "see_all": "See all",
+  "see_more_events_label": "See more events",
+  "start_date": "Start date",
+  "start_time": "Start time",
+  "title_required": "Please provide a title for the event.",
+  "upload_helper_text": "Click to begin uploading an image",
+  "virtual_event": "Virtual event",
+  "virtual_event_link": "Event link",
+  "virtual_event_location_placeholder": "View details for link",
+  "virtual_events_subtext": "Virtual events will be added to the global community.",
+  "past": "Past",
+  "upcoming": "Upcoming",
+  "your_events": "Your events",
+  "all_events_page_tabs_a11y_label": "Tabs for the events listing page",
+  "discover_events_subtitle": "Check out upcoming and future events hosted by Couchers.org community members!",
+  "discover_events_title": "Discover Couchers.org events",
+  "created_by_me": "Created by me"
 }
diff --git a/app/web/features/dashboard/MyEvents.tsx b/app/web/features/dashboard/MyEvents.tsx
index 49992c2014..82f1a7487f 100644
--- a/app/web/features/dashboard/MyEvents.tsx
+++ b/app/web/features/dashboard/MyEvents.tsx
@@ -61,7 +61,7 @@ export default function MyEvents() {
 
   const { data, error, fetchNextPage, hasNextPage, isFetching, isLoading } =
     useInfiniteQuery<ListMyEventsRes.AsObject, RpcError>({
-      queryKey: myEventsKey,
+      queryKey: myEventsKey("upcoming"),
       queryFn: ({ pageParam }) =>
         service.events.listMyEvents({
           pageToken: pageParam,
diff --git a/app/web/features/queryKeys.ts b/app/web/features/queryKeys.ts
index e4b6fe7825..b0427f34e3 100644
--- a/app/web/features/queryKeys.ts
+++ b/app/web/features/queryKeys.ts
@@ -146,4 +146,4 @@ export const searchQueryKey = (query?: string) =>
 
 // User
 export const userCommunitiesKey = "userCommunities";
-export const myEventsKey = "myEvents";
+export const myEventsKey = (type: EventsType) => ["myEvents", { type }];
diff --git a/app/web/service/events.ts b/app/web/service/events.ts
index ca64e331e6..5c54f2df67 100644
--- a/app/web/service/events.ts
+++ b/app/web/service/events.ts
@@ -249,19 +249,40 @@ export async function listAllEvents({
   return res.toObject();
 }
 
-interface ListMyEventsInput {
+export interface ListMyEventsInput {
+  pageNumber?: number;
   pageSize?: number;
   pageToken?: string;
+  pastEvents?: boolean;
+  showCancelled?: boolean;
 }
 
-export async function listMyEvents({ pageSize, pageToken }: ListMyEventsInput) {
+export async function listMyEvents({
+  pageNumber,
+  pageSize,
+  pageToken,
+  pastEvents,
+  showCancelled,
+}: ListMyEventsInput) {
   const req = new ListMyEventsReq();
+  req.setAttending(true);
+  req.setOrganizing(true);
+
+  if (pastEvents !== undefined) {
+    req.setPast(pastEvents);
+  }
+  if (pageNumber) {
+    req.setPageNumber(pageNumber);
+  }
   if (pageSize) {
     req.setPageSize(pageSize);
   }
   if (pageToken) {
     req.setPageToken(pageToken);
   }
+  if (showCancelled !== undefined) {
+    req.setIncludeCancelled(showCancelled);
+  }
 
   const res = await client.events.listMyEvents(req);
   return res.toObject();
diff --git a/app/web/service/search.ts b/app/web/service/search.ts
index fa0c65ac1b..6272a964bc 100644
--- a/app/web/service/search.ts
+++ b/app/web/service/search.ts
@@ -6,8 +6,14 @@ import {
   UInt32Value,
 } from "google-protobuf/google/protobuf/wrappers_pb";
 import { HostingStatus } from "proto/api_pb";
-import { RectArea, UserSearchReq } from "proto/search_pb";
+import {
+  EventSearchReq,
+  EventSearchRes,
+  RectArea,
+  UserSearchReq,
+} from "proto/search_pb";
 import client from "service/client";
+import { GeocodeResult } from "utils/hooks";
 
 export interface UserSearchFilters {
   query?: string;
@@ -68,3 +74,52 @@ export async function userSearch(
   const response = await client.search.userSearch(req);
   return response.toObject();
 }
+
+export async function EventSearch({
+  pageNumber,
+  pageSize,
+  pastEvents,
+  isMyCommunities,
+  isOnlineOnly,
+  searchLocation,
+}: {
+  pageNumber: number;
+  pageSize: number;
+  pastEvents?: boolean;
+  isMyCommunities?: boolean;
+  isOnlineOnly?: boolean;
+  searchLocation?: GeocodeResult | "";
+}): Promise<EventSearchRes.AsObject> {
+  const req = new EventSearchReq();
+  req.setPageSize(pageSize);
+  req.setPageNumber(pageNumber);
+
+  if (pastEvents !== undefined) {
+    req.setPast(pastEvents);
+  }
+  if (typeof searchLocation !== "string") {
+    // If it's a region (i.e. "France" or "United States") use query search by name
+    // This will search for events in the region by that name
+    if (searchLocation?.isRegion) {
+      req.setQuery(new StringValue().setValue(searchLocation.name));
+    } else {
+      // Otherwise use rectangle search so we get the area around a city
+      // This is because if you search a small town, you might want to search around it too
+      const location = new RectArea();
+      location.setLatMin(searchLocation?.bbox[1] || 0);
+      location.setLatMax(searchLocation?.bbox[3] || 0);
+      location.setLngMin(searchLocation?.bbox[0] || 0);
+      location.setLngMax(searchLocation?.bbox[2] || 0);
+      req.setSearchInRectangle(location);
+    }
+  }
+  if (isMyCommunities !== undefined) {
+    req.setMyCommunities(isMyCommunities);
+  }
+  if (isOnlineOnly !== undefined) {
+    req.setOnlyOnline(isOnlineOnly);
+  }
+
+  const res = await client.search.eventSearch(req);
+  return res.toObject();
+}


Test patch:
diff --git a/app/web/features/communities/events/EventsList.test.tsx b/app/web/features/communities/events/CommunityEventsList.test.tsx
similarity index 90%
rename from app/web/features/communities/events/EventsList.test.tsx
rename to app/web/features/communities/events/CommunityEventsList.test.tsx
index d9b69b8004..d6df963cd2 100644
--- a/app/web/features/communities/events/EventsList.test.tsx
+++ b/app/web/features/communities/events/CommunityEventsList.test.tsx
@@ -15,7 +15,7 @@ import wrapper from "test/hookWrapper";
 import { getUser } from "test/serviceMockDefaults";
 import { assertErrorAlert, mockConsoleError, t } from "test/utils";
 
-import EventsList from "./EventsList";
+import CommunityEventsList from "./CommunityEventsList";
 
 const listCommunityEventsMock = service.events
   .listCommunityEvents as jest.MockedFunction<
@@ -45,7 +45,7 @@ describe("Events list", () => {
   });
 
   it("renders the events list successfully", async () => {
-    render(<EventsList community={community} />, { wrapper });
+    render(<CommunityEventsList community={community} />, { wrapper });
     await waitForElementToBeRemoved(screen.getByRole("progressbar"));
 
     expect(
@@ -63,7 +63,7 @@ describe("Events list", () => {
       eventsList: [],
       nextPageToken: "",
     });
-    render(<EventsList community={community} />, { wrapper });
+    render(<CommunityEventsList community={community} />, { wrapper });
     await waitForElementToBeRemoved(screen.getByRole("progressbar"));
 
     expect(screen.getByText(t("communities:events_empty_state"))).toBeVisible();
@@ -72,7 +72,7 @@ describe("Events list", () => {
   it(`takes user to the page if the "${t(
     "communities:create_an_event"
   )}" button is clicked`, async () => {
-    render(<EventsList community={community} />, { wrapper });
+    render(<CommunityEventsList community={community} />, { wrapper });
 
     userEvent.click(
       screen.getByRole("button", { name: t("communities:create_an_event") })
@@ -87,7 +87,7 @@ describe("Events list", () => {
     mockConsoleError();
     const errorMessage = "Error loading community events";
     listCommunityEventsMock.mockRejectedValue(new Error(errorMessage));
-    render(<EventsList community={community} />, { wrapper });
+    render(<CommunityEventsList community={community} />, { wrapper });
 
     await waitForElementToBeRemoved(screen.getByRole("progressbar"));
 
@@ -105,7 +105,7 @@ describe("Events list", () => {
           nextPageToken: pageToken ? "" : "2",
         };
       });
-      render(<EventsList community={community} />, { wrapper });
+      render(<CommunityEventsList community={community} />, { wrapper });
 
       await waitForElementToBeRemoved(screen.getByRole("progressbar"));
       expect(screen.getAllByRole("link")).toHaveLength(2);
diff --git a/app/web/features/communities/events/LongEventCard.test.tsx b/app/web/features/communities/events/CommunityLongEventCard.test.tsx
similarity index 89%
rename from app/web/features/communities/events/LongEventCard.test.tsx
rename to app/web/features/communities/events/CommunityLongEventCard.test.tsx
index 2e9af58ee3..eba7f418ff 100644
--- a/app/web/features/communities/events/LongEventCard.test.tsx
+++ b/app/web/features/communities/events/CommunityLongEventCard.test.tsx
@@ -7,7 +7,7 @@ import { getUser } from "test/serviceMockDefaults";
 import { t } from "test/utils";
 import timezoneMock from "timezone-mock";
 
-import LongEventCard from "./LongEventCard";
+import CommunityLongEventCard from "./CommunityLongEventCard";
 
 const [firstEvent, secondEvent, thirdEvent] = events;
 
@@ -34,7 +34,7 @@ describe("Long event card", () => {
   });
 
   it("renders an offline event with the same start and end day correctly", async () => {
-    render(<LongEventCard event={firstEvent} />, { wrapper });
+    render(<CommunityLongEventCard event={firstEvent} />, { wrapper });
 
     expect(
       screen.getByRole("heading", { name: firstEvent.title })
@@ -55,7 +55,7 @@ describe("Long event card", () => {
   });
 
   it("renders an online event correctly", async () => {
-    render(<LongEventCard event={secondEvent} />, { wrapper });
+    render(<CommunityLongEventCard event={secondEvent} />, { wrapper });
 
     expect(
       screen.getByRole("heading", { name: secondEvent.title })
@@ -69,7 +69,7 @@ describe("Long event card", () => {
       attendeeUserIdsList: [3, 4, 5],
       nextPageToken: "",
     });
-    render(<LongEventCard event={thirdEvent} />, { wrapper });
+    render(<CommunityLongEventCard event={thirdEvent} />, { wrapper });
 
     const eventImage = await screen.findByRole("img", { name: "" });
     expect(eventImage).toBeVisible();
diff --git a/app/web/features/communities/events/DiscoverEventsList.test.tsx b/app/web/features/communities/events/DiscoverEventsList.test.tsx
new file mode 100644
index 0000000000..81c533ed45
--- /dev/null
+++ b/app/web/features/communities/events/DiscoverEventsList.test.tsx
@@ -0,0 +1,271 @@
+import { Pagination } from "@material-ui/lab";
+import { fireEvent, render, screen, waitFor } from "@testing-library/react";
+import { useAuthContext } from "features/auth/AuthProvider";
+import { useTranslation } from "i18n";
+import { LngLat } from "maplibre-gl";
+import React from "react";
+import { useController, useForm } from "react-hook-form";
+import mockEvents from "test/fixtures/events.json";
+import { GeocodeResult } from "utils/hooks";
+
+import DiscoverEventsList from "./DiscoverEventsList";
+import { useEventSearch } from "./hooks";
+
+jest.mock("react-hook-form", () => ({
+  useForm: jest.fn(),
+  useController: jest.fn(),
+}));
+
+jest.mock("i18n", () => ({
+  useTranslation: jest.fn(),
+}));
+
+jest.mock("../events/hooks", () => ({
+  useEventSearch: jest.fn(),
+}));
+
+jest.mock("features/auth/AuthProvider", () => ({
+  ...jest.requireActual("features/auth/AuthProvider"),
+  useAuthContext: jest.fn(),
+}));
+
+jest.mock("@material-ui/lab", () => ({
+  ...jest.requireActual("@material-ui/lab"),
+  Pagination: jest.fn(),
+}));
+
+jest.mock("utils/hooks", () => ({
+  useGeocodeQuery: jest.fn(),
+}));
+
+jest.mock("components/LocationAutocomplete", () => {
+  const LocationAutocomplete: React.FC<{
+    value: GeocodeResult;
+    onChange: (newLocationResult: GeocodeResult) => void;
+  }> = (props) => (
+    <input
+      data-testid="location-autocomplete"
+      value={props.value?.name}
+      onChange={(e) => {
+        props.onChange({
+          simplifiedName: e.target.value,
+          name: e.target.value,
+          location: new LngLat(0, 0),
+          bbox: [0, 0, 0, 0],
+        });
+      }}
+    />
+  );
+  LocationAutocomplete.displayName = "LocationAutocomplete";
+  return LocationAutocomplete;
+});
+
+describe("DiscoverEventsList", () => {
+  const mockUseTranslation = useTranslation as jest.Mock;
+  const mockUseForm = useForm as jest.Mock;
+  const mockUseEventSearch = useEventSearch as jest.Mock;
+  const mockUseAuthContext = useAuthContext as jest.Mock;
+  const mockUseController = useController as jest.Mock;
+  const mockPagination = Pagination as jest.Mock;
+
+  beforeEach(() => {
+    mockUseTranslation.mockReturnValue({
+      t: (key: string) => key,
+    });
+    mockUseForm.mockReturnValue({
+      control: {},
+      errors: {},
+    });
+    mockUseController.mockReturnValue({
+      field: {
+        onChange: jest.fn(),
+        onBlur: jest.fn(),
+        value: "",
+      },
+    });
+
+    // Mock the return value of useAuthContext to include authState with userId
+    mockUseAuthContext.mockReturnValue({
+      authState: { authenticated: true, userId: 1 },
+    });
+  });
+
+  afterEach(() => {
+    jest.clearAllMocks();
+  });
+
+  it("Renders the component correctly with empty state", () => {
+    mockUseEventSearch.mockReturnValue({
+      data: null,
+      error: null,
+      isLoading: false,
+    });
+
+    render(<DiscoverEventsList />);
+
+    expect(
+      screen.getByText("communities:discover_events_title")
+    ).toBeInTheDocument();
+    expect(screen.getByText("communities:my_communities")).toBeInTheDocument();
+    expect(screen.getByText("communities:online")).toBeInTheDocument();
+    expect(screen.getByTestId("location-autocomplete")).toBeInTheDocument();
+    expect(
+      screen.getByText("communities:events_empty_state")
+    ).toBeInTheDocument();
+  });
+
+  it("Renders loading state", () => {
+    mockUseEventSearch.mockReturnValue({
+      data: null,
+      error: null,
+      isLoading: true,
+    });
+
+    render(<DiscoverEventsList />);
+
+    expect(screen.getByRole("progressbar")).toBeInTheDocument();
+  });
+
+  it("Renders error message when there is an error", () => {
+    mockUseEventSearch.mockReturnValue({
+      data: null,
+      error: { message: "Error occurred" },
+      isLoading: false,
+    });
+
+    render(<DiscoverEventsList />);
+
+    expect(screen.getByText("Error occurred")).toBeInTheDocument();
+  });
+
+  it("Renders events and pagination when data is available", () => {
+    mockUseEventSearch.mockReturnValue({
+      data: {
+        eventsList: mockEvents.filter((event) => !event.isCancelled),
+        totalItems: 4,
+      },
+      error: null,
+      isLoading: false,
+    });
+
+    mockPagination.mockImplementation(({ onChange }) => (
+      <button onClick={() => onChange({}, 1)}>Change Page</button>
+    ));
+
+    render(<DiscoverEventsList />);
+
+    expect(screen.getByText("Weekly Meetup")).toBeInTheDocument();
+    expect(screen.getByText("Planting Season Meetup")).toBeInTheDocument();
+    expect(
+      screen.getByText(
+        "Cherry Blossom Bike Ride around the entire Amsterdam because I like to write long event titles"
+      )
+    ).toBeInTheDocument();
+    expect(screen.getByText("Change Page")).toBeInTheDocument();
+  });
+
+  it("Handles pagination", async () => {
+    mockUseEventSearch.mockReturnValue({
+      data: {
+        eventsList: [
+          ...mockEvents,
+          ...mockEvents.map((event) => ({
+            ...event,
+            eventId: event.eventId + 4,
+          })),
+          ...mockEvents.map((event) => ({
+            ...event,
+            eventId: event.eventId + 8,
+          })),
+          ...mockEvents.map((event) => ({
+            ...event,
+            eventId: event.eventId + 16,
+          })),
+        ],
+        totalItems: 12,
+      },
+      error: null,
+      isLoading: false,
+    });
+
+    mockPagination.mockImplementation(({ onChange }) => (
+      <button onClick={() => onChange({}, 2)}>Change Page</button>
+    ));
+
+    render(<DiscoverEventsList />);
+
+    const paginationButton = screen.getByText("Change Page");
+    fireEvent.click(paginationButton);
+
+    await waitFor(() =>
+      expect(mockUseEventSearch).toHaveBeenCalledWith(
+        expect.objectContaining({ pageNumber: 2 })
+      )
+    );
+  });
+
+  it("Handles communities and online filter clicks correctly", async () => {
+    mockUseEventSearch.mockReturnValue({
+      data: mockEvents,
+      error: null,
+      isLoading: false,
+    });
+
+    render(<DiscoverEventsList />);
+
+    const communitiesFilter = screen.getByText("communities:my_communities");
+    const onlineFilter = screen.getByText("communities:online");
+    expect(communitiesFilter).toBeInTheDocument();
+    expect(onlineFilter).toBeInTheDocument();
+
+    fireEvent.click(communitiesFilter);
+
+    await waitFor(() =>
+      expect(mockUseEventSearch).toHaveBeenCalledWith(
+        expect.objectContaining({ isMyCommunities: true })
+      )
+    );
+    expect(communitiesFilter.className).toContain("selectedFilter");
+
+    fireEvent.click(onlineFilter);
+    await waitFor(() =>
+      expect(mockUseEventSearch).toHaveBeenCalledWith(
+        expect.objectContaining({ isOnlineOnly: true })
+      )
+    );
+    expect(onlineFilter.className).toContain("selectedFilter");
+  });
+
+  it("Handles location autocomplete change correctly", async () => {
+    mockUseEventSearch.mockReturnValue({
+      data: mockEvents,
+      error: null,
+      isLoading: false,
+    });
+
+    render(<DiscoverEventsList />);
+
+    const locationAutocomplete = screen.getByTestId("location-autocomplete");
+
+    const newLocation = {
+      simplifiedName: "New York, NY",
+      name: "New York, NY",
+      location: new LngLat(0, 0),
+      bbox: [0, 0, 0, 0],
+    };
+    fireEvent.change(locationAutocomplete, {
+      target: { value: "New York, NY" },
+    });
+
+    // Simulate the internal onChange handler of the autocomplete being triggered
+    fireEvent.keyDown(locationAutocomplete, { key: "Enter", code: "Enter" });
+
+    await waitFor(() => {
+      expect(mockUseEventSearch).toHaveBeenLastCalledWith(
+        expect.objectContaining({
+          searchLocation: newLocation,
+        })
+      );
+    });
+  });
+});
diff --git a/app/web/features/communities/events/EventsPage.test.tsx b/app/web/features/communities/events/EventsPage.test.tsx
index fb7f9a9d40..2f38bb6a33 100644
--- a/app/web/features/communities/events/EventsPage.test.tsx
+++ b/app/web/features/communities/events/EventsPage.test.tsx
@@ -1,158 +1,24 @@
-import {
-  render,
-  screen,
-  waitFor,
-  waitForElementToBeRemoved,
-  within,
-} from "@testing-library/react";
+import { render, screen, waitFor } from "@testing-library/react";
 import userEvent from "@testing-library/user-event";
 import mockRouter from "next-router-mock";
 import { routeToNewEvent } from "routes";
-import { service } from "service";
-import events from "test/fixtures/events.json";
 import wrapper from "test/hookWrapper";
-import { assertErrorAlert, mockConsoleError, t } from "test/utils";
+import { t } from "test/utils";
 
 import EventsPage from "./EventsPage";
 
-const listAllEventsMock = service.events.listAllEvents as jest.MockedFunction<
-  typeof service.events.listAllEvents
->;
-
 describe("Events page", () => {
-  it("shows the 'Upcoming' tab by default", async () => {
-    listAllEventsMock.mockResolvedValue({
-      eventsList: events.filter((event) => !event.isCancelled),
-      nextPageToken: "",
-    });
-    render(<EventsPage />, { wrapper });
-
-    expect(
-      screen.getByRole("heading", {
-        name: t("communities:discover_events_title"),
-      })
-    ).toBeVisible();
-    expect(
-      screen.getByText(t("communities:discover_events_subtitle"))
-    ).toBeVisible();
-    expect(
-      await screen.findByRole("heading", { name: t("communities:upcoming") })
-    ).toBeVisible();
-
-    // Check that there are 3 events cards in success case (by default 1 cancelled not shown)
-    expect(screen.getAllByRole("link")).toHaveLength(3);
-  });
-
-  it("renders the empty state if there are no events", async () => {
-    listAllEventsMock.mockResolvedValue({
-      eventsList: [],
-      nextPageToken: "",
-    });
-    render(<EventsPage />, { wrapper });
-    await waitForElementToBeRemoved(screen.getByRole("progressbar"));
-
-    expect(screen.getByText(t("communities:events_empty_state"))).toBeVisible();
-    // Check that there are no events card, in addition to empty state
-    expect(screen.queryByRole("link")).not.toBeInTheDocument();
-  });
-
-  it('switches to the "Past" tab when clicked', async () => {
-    render(<EventsPage />, { wrapper });
-
-    userEvent.click(screen.getByRole("tab", { name: t("communities:past") }));
-
-    expect(
-      await screen.findByRole("heading", { name: t("communities:past") })
-    ).toBeVisible();
-  });
-
   it(`takes user to the page if the "${t(
     "communities:create_an_event"
   )}" button is clicked`, async () => {
     render(<EventsPage />, { wrapper });
 
     userEvent.click(
-      screen.getByRole("button", { name: t("communities:create_an_event") })
+      screen.getByRole("button", { name: t("communities:create_new_event") })
     );
 
     await waitFor(() => {
       expect(mockRouter.asPath).toBe(routeToNewEvent());
     });
   });
-
-  it("shows an error alert if the events failed to load", async () => {
-    mockConsoleError();
-    const errorMessage = "Error listing all events";
-    listAllEventsMock.mockRejectedValue(new Error(errorMessage));
-    render(<EventsPage />, { wrapper });
-
-    await waitForElementToBeRemoved(screen.getByRole("progressbar"));
-
-    await assertErrorAlert(errorMessage);
-    expect(
-      screen.queryByText(t("communities:events_empty_state"))
-    ).not.toBeInTheDocument();
-  });
-
-  it(`shows cancelled events if "${t(
-    "communities:show_cancelled_events"
-  )}" is checked`, async () => {
-    listAllEventsMock.mockResolvedValue({
-      eventsList: events.filter((event) => !event.isCancelled),
-      nextPageToken: "",
-    });
-
-    render(<EventsPage />, { wrapper });
-    await waitForElementToBeRemoved(screen.getByRole("progressbar"));
-
-    expect(listAllEventsMock).toHaveBeenCalledWith({
-      pastEvents: false,
-      showCancelled: false,
-    });
-
-    expect(screen.getAllByRole("link")).toHaveLength(3);
-
-    const switchSpan = screen.getByRole("checkbox", {
-      name: t("communities:show_cancelled_events"),
-    });
-
-    userEvent.click(switchSpan);
-
-    // Check that listAllEvents is called with showCancelled: true
-    expect(listAllEventsMock).toHaveBeenCalledWith({
-      pastEvents: false,
-      showCancelled: true,
-    });
-  });
-
-  describe("when there are more than one page of events", () => {
-    it(`shows the next page of events when the "${t(
-      "communities:see_more_events_label"
-    )}" button is clicked`, async () => {
-      listAllEventsMock.mockImplementation(async ({ pageToken }) => {
-        return {
-          eventsList: pageToken ? events.slice(2) : events.slice(0, 2),
-          nextPageToken: pageToken ? "" : "2",
-        };
-      });
-      render(<EventsPage />, { wrapper });
-      await waitForElementToBeRemoved(screen.getByRole("progressbar"));
-      expect(screen.getAllByRole("link")).toHaveLength(2);
-
-      const seeMoreEventsButton = screen.getByRole("button", {
-        name: t("communities:see_more_events_label"),
-      });
-      userEvent.click(seeMoreEventsButton);
-      await waitForElementToBeRemoved(
-        within(seeMoreEventsButton).getByRole("progressbar")
-      );
-
-      expect(screen.getAllByRole("link")).toHaveLength(4);
-      expect(listAllEventsMock).toHaveBeenCalledTimes(2);
-      expect(listAllEventsMock.mock.calls).toEqual([
-        [{ pastEvents: false, showCancelled: false }],
-        [{ pastEvents: false, pageToken: "2", showCancelled: false }],
-      ]);
-    });
-  });
 });
diff --git a/app/web/features/communities/events/MyEventsList.test.tsx b/app/web/features/communities/events/MyEventsList.test.tsx
new file mode 100644
index 0000000000..2b7a86b70f
--- /dev/null
+++ b/app/web/features/communities/events/MyEventsList.test.tsx
@@ -0,0 +1,199 @@
+import { Pagination } from "@material-ui/lab";
+import { fireEvent, render, screen, waitFor } from "@testing-library/react";
+import { useAuthContext } from "features/auth/AuthProvider";
+import { useTranslation } from "i18n";
+import mockEvents from "test/fixtures/events.json";
+
+import { useListMyEvents } from "./hooks";
+import MyEventsList from "./MyEventsList";
+
+jest.mock("i18n", () => ({
+  useTranslation: jest.fn(),
+}));
+
+jest.mock("../events/hooks", () => ({
+  useListMyEvents: jest.fn(),
+}));
+
+jest.mock("features/auth/AuthProvider", () => ({
+  ...jest.requireActual("features/auth/AuthProvider"),
+  useAuthContext: jest.fn(),
+}));
+
+jest.mock("@material-ui/lab", () => ({
+  ...jest.requireActual("@material-ui/lab"),
+  Pagination: jest.fn(),
+}));
+
+describe("MyEventsList", () => {
+  const mockUseTranslation = useTranslation as jest.Mock;
+  const mockUseListMyEvents = useListMyEvents as jest.Mock;
+  const mockPagination = Pagination as jest.Mock;
+
+  beforeEach(() => {
+    mockUseTranslation.mockReturnValue({
+      t: (key: string) => key,
+    });
+
+    // Mock the return value of useAuthContext to include authState with userId
+    (useAuthContext as jest.Mock).mockReturnValue({
+      authState: { authenticated: true, userId: 1 },
+    });
+  });
+
+  afterEach(() => {
+    jest.resetAllMocks();
+  });
+
+  it("Renders loading state", () => {
+    mockUseListMyEvents.mockReturnValue({
+      data: null,
+      error: null,
+      isLoading: true,
+    });
+
+    render(<MyEventsList />);
+
+    expect(screen.getByRole("progressbar")).toBeInTheDocument();
+  });
+
+  it("Renders error state", () => {
+    mockUseListMyEvents.mockReturnValue({
+      data: null,
+      error: { message: "Error loading events" },
+      isLoading: false,
+    });
+
+    render(<MyEventsList />);
+
+    expect(screen.getByText("Error loading events")).toBeInTheDocument();
+  });
+
+  it("Renders empty state when no events", () => {
+    mockUseListMyEvents.mockReturnValue({
+      data: { eventsList: [], totalItems: 0 },
+      error: null,
+      isLoading: false,
+    });
+
+    render(<MyEventsList />);
+
+    expect(
+      screen.getByText("communities:events_empty_state")
+    ).toBeInTheDocument();
+  });
+
+  it("Renders events list when events are available", () => {
+    mockUseListMyEvents.mockReturnValue({
+      data: {
+        eventsList: mockEvents.filter((event) => !event.isCancelled),
+        totalItems: 3,
+      },
+      error: null,
+      isLoading: false,
+    });
+
+    mockPagination.mockImplementation(({ onChange }) => (
+      <button onClick={() => onChange({}, 1)}>Change Page</button>
+    ));
+
+    render(<MyEventsList />);
+
+    expect(screen.getByText("Planting Season Meetup")).toBeInTheDocument();
+
+    // check that EventItem component appears 3 times
+    expect(screen.getAllByTestId("event-item")).toHaveLength(3);
+  });
+
+  it("Can toggle past events filter", async () => {
+    mockUseListMyEvents.mockReturnValue({
+      data: { eventsList: mockEvents, totalItems: 0 },
+      error: null,
+      isLoading: false,
+    });
+
+    mockPagination.mockImplementation(({ onChange }) => (
+      <button onClick={() => onChange({}, 1)}>Change Page</button>
+    ));
+
+    render(<MyEventsList />);
+
+    const pastFilter = screen.getByText("communities:past");
+    expect(pastFilter).toBeInTheDocument();
+
+    fireEvent.click(pastFilter);
+
+    await waitFor(() =>
+      expect(mockUseListMyEvents).toHaveBeenCalledWith(
+        expect.objectContaining({ pastEvents: true })
+      )
+    );
+  });
+
+  it("Can toggle show cancelled filter", async () => {
+    mockUseListMyEvents.mockReturnValue({
+      data: { eventsList: mockEvents, totalItems: 0 },
+      error: null,
+      isLoading: false,
+    });
+
+    mockPagination.mockImplementation(({ onChange }) => (
+      <button onClick={() => onChange({}, 1)}>Change Page</button>
+    ));
+
+    render(<MyEventsList />);
+
+    const cancelledFilter = screen.getByText(
+      "communities:show_cancelled_events"
+    );
+    expect(cancelledFilter).toBeInTheDocument();
+
+    fireEvent.click(cancelledFilter);
+
+    await waitFor(() =>
+      expect(mockUseListMyEvents).toHaveBeenCalledWith(
+        expect.objectContaining({ showCancelled: true })
+      )
+    );
+  });
+
+  it("Handles pagination", async () => {
+    mockUseListMyEvents.mockReturnValue({
+      data: {
+        eventsList: [
+          ...mockEvents,
+          ...mockEvents.map((event) => ({
+            ...event,
+            eventId: event.eventId + 4,
+          })),
+          ...mockEvents.map((event) => ({
+            ...event,
+            eventId: event.eventId + 8,
+          })),
+          ...mockEvents.map((event) => ({
+            ...event,
+            eventId: event.eventId + 16,
+          })),
+        ],
+        totalItems: 12,
+      },
+      error: null,
+      isLoading: false,
+    });
+
+    mockPagination.mockImplementation(({ onChange }) => (
+      <button onClick={() => onChange({}, 2)}>Change Page</button>
+    ));
+
+    render(<MyEventsList />);
+
+    const paginationButton = screen.getByText("Change Page");
+    fireEvent.click(paginationButton);
+
+    await waitFor(() =>
+      expect(mockUseListMyEvents).toHaveBeenCalledWith(
+        expect.objectContaining({ pageNumber: 2 })
+      )
+    );
+  });
+});