Skip to content

Commit

Permalink
Merge pull request #136 from eurofurence/feature/dealer-keywords
Browse files Browse the repository at this point in the history
feat: dealer keywords and home refactoring
Requinard authored Aug 24, 2024
2 parents 4a4e8e5 + e379e17 commit 746dff7
Showing 16 changed files with 225 additions and 81 deletions.
46 changes: 38 additions & 8 deletions src/components/dealers/DealerContent.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as Linking from "expo-linking";
import { TFunction } from "i18next";
import moment from "moment";
import React, { FC, useCallback, useMemo } from "react";
import { useTranslation } from "react-i18next";
@@ -23,6 +24,41 @@ import { Button } from "../generic/containers/Button";
import { ImageExButton } from "../generic/containers/ImageButton";
import { LinkItem } from "../maps/LinkItem";

const DealerCategories = ({ t, dealer }: { t: TFunction; dealer: DealerDetails }) => {
// Nothing to display for no categories.
if (!dealer.Categories?.length) return null;

return (
<>
<Label type="caption">{t("categories")}</Label>
<View style={dealerCategoriesStyles.container}>
{dealer.Categories.map((category: string) => {
const keywords = dealer.Keywords?.[category];
if (keywords?.length)
return (
<Label key={category} mt={5}>
<Label type="strong">{category}: </Label>
{keywords.join(", ")}
</Label>
);
else
return (
<Label key={category} type="strong">
{category}
</Label>
);
})}
</View>
</>
);
};

const dealerCategoriesStyles = StyleSheet.create({
container: {
marginBottom: 20,
},
});

/**
* Props to the content.
*/
@@ -133,14 +169,8 @@ export const DealerContent: FC<DealerContentProps> = ({ dealer, parentPad = 0, u
</>
)}

{!dealer.Categories?.length ? null : (
<>
<Label type="caption">{t("categories")}</Label>
<Label type="h3" mb={20}>
{dealer.Categories?.join(", ")}
</Label>
</>
)}
<DealerCategories t={t} dealer={dealer} />

{dealer.Links &&
dealer.Links.map((it) => (
<View style={styles.button} key={it.Name}>
5 changes: 3 additions & 2 deletions src/components/dealers/DealersList.tsx
Original file line number Diff line number Diff line change
@@ -25,16 +25,17 @@ export type DealersListProps = {
dealers: DealerDetailsInstance[];
empty?: ReactElement;
trailer?: ReactElement;
padEnd?: boolean;
};

export const DealersList: FC<DealersListProps> = ({ navigation, leader, dealers, empty, trailer }) => {
export const DealersList: FC<DealersListProps> = ({ navigation, leader, dealers, empty, trailer, padEnd = true }) => {
const theme = useThemeName();
const synchronizer = useSynchronizer();
return (
<FlashList
refreshing={synchronizer.isSynchronizing}
onRefresh={synchronizer.synchronize}
contentContainerStyle={styles.container}
contentContainerStyle={padEnd ? styles.container : undefined}
scrollEnabled={true}
ListHeaderComponent={leader}
ListFooterComponent={trailer}
5 changes: 3 additions & 2 deletions src/components/dealers/DealersSectionedList.tsx
Original file line number Diff line number Diff line change
@@ -28,9 +28,10 @@ export type DealersSectionedListProps = {
empty?: ReactElement;
trailer?: ReactElement;
sticky?: boolean;
padEnd?: boolean;
};

export const DealersSectionedList: FC<DealersSectionedListProps> = ({ navigation, leader, dealersGroups, empty, trailer, sticky = true }) => {
export const DealersSectionedList: FC<DealersSectionedListProps> = ({ navigation, leader, dealersGroups, empty, trailer, sticky = true, padEnd = true }) => {
const theme = useThemeName();
const synchronizer = useSynchronizer();
const stickyIndices = useMemo(() => (sticky ? findIndices(dealersGroups, (item) => !("details" in item)) : undefined), [dealersGroups, sticky]);
@@ -39,7 +40,7 @@ export const DealersSectionedList: FC<DealersSectionedListProps> = ({ navigation
<FlashList
refreshing={synchronizer.isSynchronizing}
onRefresh={synchronizer.synchronize}
contentContainerStyle={styles.container}
contentContainerStyle={padEnd ? styles.container : undefined}
scrollEnabled={true}
stickyHeaderIndices={stickyIndices}
ListHeaderComponent={leader}
5 changes: 3 additions & 2 deletions src/components/events/EventsList.tsx
Original file line number Diff line number Diff line change
@@ -28,16 +28,17 @@ export type EventsListProps = {
empty?: ReactElement;
trailer?: ReactElement;
cardType?: "duration" | "time";
padEnd?: boolean;
};

export const EventsList: FC<EventsListProps> = ({ navigation, leader, events, select, empty, trailer, cardType = "duration" }) => {
export const EventsList: FC<EventsListProps> = ({ navigation, leader, events, select, empty, trailer, cardType = "duration", padEnd = true }) => {
const theme = useThemeName();
const synchronizer = useSynchronizer();
return (
<FlashList
refreshing={synchronizer.isSynchronizing}
onRefresh={synchronizer.synchronize}
contentContainerStyle={styles.container}
contentContainerStyle={padEnd ? styles.container : undefined}
scrollEnabled={true}
ListHeaderComponent={leader}
ListFooterComponent={trailer}
15 changes: 13 additions & 2 deletions src/components/events/EventsSectionedList.tsx
Original file line number Diff line number Diff line change
@@ -31,9 +31,20 @@ export type EventsSectionedListProps = {
trailer?: ReactElement;
cardType?: "duration" | "time";
sticky?: boolean;
padEnd?: boolean;
};

export const EventsSectionedList: FC<EventsSectionedListProps> = ({ navigation, leader, eventsGroups, select, empty, trailer, cardType = "duration", sticky = true }) => {
export const EventsSectionedList: FC<EventsSectionedListProps> = ({
navigation,
leader,
eventsGroups,
select,
empty,
trailer,
cardType = "duration",
sticky = true,
padEnd = true,
}) => {
const theme = useThemeName();
const synchronizer = useSynchronizer();
const stickyIndices = useMemo(() => (sticky ? findIndices(eventsGroups, (item) => !("details" in item)) : undefined), [eventsGroups, sticky]);
@@ -42,7 +53,7 @@ export const EventsSectionedList: FC<EventsSectionedListProps> = ({ navigation,
<FlashList
refreshing={synchronizer.isSynchronizing}
onRefresh={synchronizer.synchronize}
contentContainerStyle={styles.container}
contentContainerStyle={padEnd ? styles.container : undefined}
scrollEnabled={true}
stickyHeaderIndices={stickyIndices}
ListHeaderComponent={leader}
2 changes: 1 addition & 1 deletion src/components/events/TodayScheduleList.tsx
Original file line number Diff line number Diff line change
@@ -24,7 +24,7 @@ export const TodayScheduleList: FC<TodayScheduleListProps> = ({ now }) => {
[favorites, now],
);

if (favorites.length === 0) {
if (events.length === 0) {
return null;
}

5 changes: 5 additions & 0 deletions src/components/generic/atoms/Label.tsx
Original file line number Diff line number Diff line change
@@ -106,6 +106,11 @@ const types = StyleSheet.create({
lineHeight: 24,
fontWeight: "100",
},
xl: {
fontSize: 44,
lineHeight: 48,
fontWeight: "500",
},
h1: {
fontSize: 30,
lineHeight: 34,
58 changes: 37 additions & 21 deletions src/components/home/CountdownHeader.tsx
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@ import { TFunction } from "i18next";
import { chain } from "lodash";
import { Moment } from "moment";
import moment from "moment/moment";
import { FC } from "react";
import React, { FC } from "react";
import { useTranslation } from "react-i18next";
import { StyleProp, StyleSheet, useWindowDimensions, View, ViewStyle } from "react-native";

@@ -15,7 +15,9 @@ import { EventDayRecord } from "../../store/eurofurence/types";
import { assetSource } from "../../util/assets";
import { Image } from "../generic/atoms/Image";
import { ImageBackground } from "../generic/atoms/ImageBackground";
import { Section } from "../generic/atoms/Section";
import { Label } from "../generic/atoms/Label";
import { Col } from "../generic/containers/Col";
import { Row } from "../generic/containers/Row";

export type CountdownHeaderProps = {
style?: StyleProp<ViewStyle>;
@@ -56,49 +58,63 @@ export const CountdownHeader: FC<CountdownHeaderProps> = ({ style }) => {
const { width } = useWindowDimensions();

const subtitle = useCountdownTitle(t, now);

return (
<View style={[styles.container, style]}>
<ImageBackground
key="banner"
style={styles.background}
style={StyleSheet.absoluteFill}
source={assetSource(width < bannerBreakpoint ? "banner_narrow" : "banner_wide")}
contentFit="cover"
priority="high"
/>
<Section
style={styles.section}
title={conId}
icon="alarm"
subtitle={subtitle}
titleColor="white"
subtitleColor="white"
titleVariant="shadow"
subtitleVariant="shadow"
/>
<View style={[StyleSheet.absoluteFill, styles.cover]} />
<Image style={styles.logo} source={assetSource("banner_logo")} contentFit="contain" priority="high" />
<Col style={styles.textContainer}>
<Row type="center">
<Label style={styles.textContainerFill} type="xl" variant="shadow" color="white" ellipsizeMode="tail">
{conId}
</Label>
</Row>

{!subtitle ? null : (
<Row type="center">
<Label style={styles.textContainerFill} type="compact" variant="shadow" color="white" ellipsizeMode="tail">
{subtitle}
</Label>
</Row>
)}
</Col>
</View>
);
};

const styles = StyleSheet.create({
background: {
...StyleSheet.absoluteFillObject,
opacity: 0.6,
cover: {
backgroundColor: "#00000060",
},
container: {
minHeight: 180,
height: 160,
paddingTop: 15,
paddingHorizontal: 15,
flexDirection: "column-reverse",
},
textContainer: {
paddingTop: 30,
paddingBottom: 5,
},
textContainerFill: {
flex: 1,
},
section: {
marginTop: 0,
},
logo: {
width: "50%",
height: "auto",
maxWidth: 200,
position: "absolute",
top: 25,
right: 25,
bottom: 25,
aspectRatio: 1,
alignSelf: "center",
maxHeight: 130,
},
});
55 changes: 55 additions & 0 deletions src/components/home/GlobalSearch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Moment } from "moment/moment";
import React from "react";
import { useTranslation } from "react-i18next";

import { useDealerGroups } from "../../routes/dealers/Dealers.common";
import { useEventSearchGroups } from "../../routes/events/Events.common";
import { HomeProps } from "../../routes/home/Home";
import { DealerDetails, EventDetails, KnowledgeEntryDetails } from "../../store/eurofurence/types";
import { DealersSectionedList } from "../dealers/DealersSectionedList";
import { EventsSectionedList } from "../events/EventsSectionedList";
import { Section } from "../generic/atoms/Section";
import { KbSectionedList } from "../kb/KbSectionedList";

export type GlobalSearchProps = {
navigation: HomeProps["navigation"];
now: Moment;
results: (DealerDetails | EventDetails | KnowledgeEntryDetails)[] | null;
};

export const GlobalSearch = ({ navigation, now, results }: GlobalSearchProps) => {
const { t: tMenu } = useTranslation("Menu");
const { t: tDealers } = useTranslation("Dealers");
const { t: tEvents } = useTranslation("Events");

// Use all dealers and group generically.
const dealersGroups = useDealerGroups(tDealers, now, results?.filter((r) => "RegistrationNumber" in r) as DealerDetails[], []);
const eventGroups = useEventSearchGroups(tEvents, now, results?.filter((r) => "StartDateTimeUtc" in r) as EventDetails[]);
const kbGroups = results?.filter((r) => "KnowledgeGroupId" in r) as KnowledgeEntryDetails[];

if (!results) return null;
return (
<>
{!dealersGroups?.length ? null : (
<DealersSectionedList
navigation={navigation as any}
dealersGroups={dealersGroups}
leader={<Section icon="card-search" title={tMenu("dealers")} />}
padEnd={false}
/>
)}
{!eventGroups?.length ? null : (
<EventsSectionedList
navigation={navigation as any}
eventsGroups={eventGroups}
cardType="time"
leader={<Section icon="card-search" title={tMenu("events")} />}
padEnd={false}
/>
)}
{!kbGroups?.length ? null : (
<KbSectionedList navigation={navigation as any} kbGroups={kbGroups} leader={<Section icon="card-search" title={tMenu("info")} />} padEnd={false} />
)}
</>
);
};
5 changes: 3 additions & 2 deletions src/components/kb/KbSectionedList.tsx
Original file line number Diff line number Diff line change
@@ -21,9 +21,10 @@ export type KbSectionedListProps = {
empty?: ReactElement;
trailer?: ReactElement;
sticky?: boolean;
padEnd?: boolean;
};

export const KbSectionedList: FC<KbSectionedListProps> = ({ navigation, leader, kbGroups, empty, trailer, sticky = true }) => {
export const KbSectionedList: FC<KbSectionedListProps> = ({ navigation, leader, kbGroups, empty, trailer, sticky = true, padEnd = true }) => {
const theme = useThemeName();
const synchronizer = useSynchronizer();
const stickyIndices = useMemo(() => (sticky ? findIndices(kbGroups, (item) => !("KnowledgeGroupId" in item)) : undefined), [kbGroups, sticky]);
@@ -32,7 +33,7 @@ export const KbSectionedList: FC<KbSectionedListProps> = ({ navigation, leader,
<FlashList
refreshing={synchronizer.isSynchronizing}
onRefresh={synchronizer.synchronize}
contentContainerStyle={styles.container}
contentContainerStyle={padEnd ? styles.container : undefined}
scrollEnabled={true}
stickyHeaderIndices={stickyIndices}
ListHeaderComponent={leader}
2 changes: 1 addition & 1 deletion src/components/settings/ThemePicker.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import _, { capitalize } from "lodash";
import { capitalize } from "lodash";
import React from "react";
import { useTranslation } from "react-i18next";
import { StyleSheet } from "react-native";
2 changes: 1 addition & 1 deletion src/components/viewer/ViewerImageRecord.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ReactNativeZoomableView as ZoomableView } from "@openspacelabs/react-native-zoomable-view";
import { FC } from "react";
import { useTranslation } from "react-i18next";
import { Dimensions, StyleSheet, View } from "react-native";
import { StyleSheet, View } from "react-native";

import { minZoomFor, shareImage } from "./Viewer.common";
import { useAppSelector } from "../../store";
2 changes: 1 addition & 1 deletion src/components/viewer/ViewerUrl.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ReactNativeZoomableView as ZoomableView } from "@openspacelabs/react-native-zoomable-view";
import { FC, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Dimensions, StyleSheet, View, Image as ReactImage } from "react-native";
import { StyleSheet, View, Image as ReactImage } from "react-native";

import { minZoomFor, shareImage } from "./Viewer.common";
import { Image } from "../generic/atoms/Image";
26 changes: 20 additions & 6 deletions src/routes/home/Home.tsx
Original file line number Diff line number Diff line change
@@ -10,14 +10,18 @@ import { AnnouncementList } from "../../components/announce/AnnouncementList";
import { CurrentEventList } from "../../components/events/CurrentEventsList";
import { TodayScheduleList } from "../../components/events/TodayScheduleList";
import { UpcomingEventsList } from "../../components/events/UpcomingEventsList";
import { Search } from "../../components/generic/atoms/Search";
import { Floater, padFloater } from "../../components/generic/containers/Floater";
import { CountdownHeader } from "../../components/home/CountdownHeader";
import { DeviceSpecificWarnings } from "../../components/home/DeviceSpecificWarnings";
import { FavoritesChangedWarning } from "../../components/home/FavoritesChangedWarning";
import { GlobalSearch } from "../../components/home/GlobalSearch";
import { LanguageWarnings } from "../../components/home/LanguageWarnings";
import { TimezoneWarning } from "../../components/home/TimezoneWarning";
import { useSynchronizer } from "../../components/sync/SynchronizationProvider";
import { useFuseIntegration } from "../../hooks/searching/useFuseIntegration";
import { useNow } from "../../hooks/time/useNow";
import { selectGlobalSearchIndex } from "../../store/eurofurence/selectors/search";
import { AreasRouterParamsList } from "../AreasRouter";
import { IndexRouterParamsList } from "../IndexRouter";

@@ -31,23 +35,33 @@ export type HomeParams = undefined;
*/
export type HomeProps = CompositeScreenProps<BottomTabScreenProps<AreasRouterParamsList, "Home">, StackScreenProps<IndexRouterParamsList>>;

export const Home: FC<HomeProps> = () => {
export const Home: FC<HomeProps> = ({ navigation }) => {
const isFocused = useIsFocused();
const now = useNow(isFocused ? 5 : "static");

// Search integration.
const [filter, setFilter, results] = useFuseIntegration(selectGlobalSearchIndex);

const { synchronize, isSynchronizing } = useSynchronizer();
return (
<ScrollView style={StyleSheet.absoluteFill} refreshControl={<RefreshControl refreshing={isSynchronizing} onRefresh={synchronize} />}>
<ScrollView style={[StyleSheet.absoluteFill]} refreshControl={<RefreshControl refreshing={isSynchronizing} onRefresh={synchronize} />}>
<CountdownHeader />
<Floater contentStyle={appStyles.trailer}>
<LanguageWarnings parentPad={padFloater} />
<TimezoneWarning parentPad={padFloater} />
<DeviceSpecificWarnings />
<FavoritesChangedWarning />
<AnnouncementList now={now} />
<UpcomingEventsList now={now} />
<TodayScheduleList now={now} />
<CurrentEventList now={now} />
<Search filter={filter} setFilter={setFilter} />
{results ? (
<GlobalSearch navigation={navigation} now={now} results={results} />
) : (
<>
<AnnouncementList now={now} />
<UpcomingEventsList now={now} />
<TodayScheduleList now={now} />
<CurrentEventList now={now} />
</>
)}
</Floater>
</ScrollView>
);
72 changes: 40 additions & 32 deletions src/store/eurofurence/selectors/search.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
import { createSelector } from "@reduxjs/toolkit";
import Fuse from "fuse.js";
import { flatten } from "lodash";

import { selectDealersInAd, selectDealersInRegular } from "./dealers";
import { dealersSelectors, eventsSelector, knowledgeEntriesSelectors } from "./records";
import { DealerDetails, EventDetails, KnowledgeEntryDetails } from "../types";

/**
* Search options.
*/
const searchOptions: Fuse.IFuseOptions<any> = {
shouldSort: true,
threshold: 0.3,
};

/**
* Properties to use in search.
*/
@@ -17,6 +26,11 @@ const dealerSearchProperties: Fuse.FuseOptionKey<DealerDetails>[] = [
name: "Categories",
weight: 1,
},
{
name: "Keywords",
getFn: (details) => (details.Keywords ? flatten(Object.values(details.Keywords)) : []),
weight: 1,
},
{
name: "ShortDescription",
weight: 1,
@@ -30,22 +44,9 @@ const dealerSearchProperties: Fuse.FuseOptionKey<DealerDetails>[] = [
weight: 1,
},
];
/**
* Search options.
*/
const dealerSearchOptions: Fuse.IFuseOptions<DealerDetails> = {
shouldSort: true,
threshold: 0.3,
};
export const selectDealersAllSearchIndex = createSelector(
[dealersSelectors.selectAll],
(data) => new Fuse(data, dealerSearchOptions, Fuse.createIndex(dealerSearchProperties, data)),
);
export const selectDealersInRegularSearchIndex = createSelector(
[selectDealersInRegular],
(data) => new Fuse(data, dealerSearchOptions, Fuse.createIndex(dealerSearchProperties, data)),
);
export const selectDealersInAdSearchIndex = createSelector([selectDealersInAd], (data) => new Fuse(data, dealerSearchOptions, Fuse.createIndex(dealerSearchProperties, data)));
export const selectDealersAllSearchIndex = createSelector([dealersSelectors.selectAll], (data) => new Fuse(data, searchOptions, Fuse.createIndex(dealerSearchProperties, data)));
export const selectDealersInRegularSearchIndex = createSelector([selectDealersInRegular], (data) => new Fuse(data, searchOptions, Fuse.createIndex(dealerSearchProperties, data)));
export const selectDealersInAdSearchIndex = createSelector([selectDealersInAd], (data) => new Fuse(data, searchOptions, Fuse.createIndex(dealerSearchProperties, data)));
/**
* Properties to use in search.
*/
@@ -79,14 +80,8 @@ const eventSearchProperties: Fuse.FuseOptionKey<EventDetails>[] = [
weight: 0.1,
},
];
/**
* Search options.
*/
const eventSearchOptions: Fuse.IFuseOptions<EventDetails> = {
shouldSort: true,
threshold: 0.3,
};
export const selectEventsAllSearchIndex = createSelector([eventsSelector.selectAll], (data) => new Fuse(data, eventSearchOptions, Fuse.createIndex(eventSearchProperties, data)));

export const selectEventsAllSearchIndex = createSelector([eventsSelector.selectAll], (data) => new Fuse(data, searchOptions, Fuse.createIndex(eventSearchProperties, data)));
/**
* Properties to use in search.
*/
@@ -100,11 +95,24 @@ const kbSearchProperties: Fuse.FuseOptionKey<KnowledgeEntryDetails>[] = [
weight: 1,
},
];
/**
* Search options.
*/
const kbSearchOptions: Fuse.IFuseOptions<KnowledgeEntryDetails> = {
shouldSort: true,
threshold: 0.3,
};
export const selectKbAllSearchIndex = createSelector([knowledgeEntriesSelectors.selectAll], (data) => new Fuse(data, kbSearchOptions, Fuse.createIndex(kbSearchProperties, data)));

export const selectKbAllSearchIndex = createSelector([knowledgeEntriesSelectors.selectAll], (data) => new Fuse(data, searchOptions, Fuse.createIndex(kbSearchProperties, data)));

export const selectGlobalSearchIndex = createSelector(
[dealersSelectors.selectAll, eventsSelector.selectAll, knowledgeEntriesSelectors.selectAll],
(dealers, events, knowledgeEntries) => {
const data = [...dealers, ...events, ...knowledgeEntries];
return new Fuse(
data,
searchOptions,
Fuse.createIndex(
[
...(dealerSearchProperties as Fuse.FuseOptionKey<DealerDetails | EventDetails | KnowledgeEntryDetails>[]),
...(eventSearchProperties as Fuse.FuseOptionKey<DealerDetails | EventDetails | KnowledgeEntryDetails>[]),
...(kbSearchProperties as Fuse.FuseOptionKey<DealerDetails | EventDetails | KnowledgeEntryDetails>[]),
],
data,
),
);
},
);
1 change: 1 addition & 0 deletions src/store/eurofurence/types.ts
Original file line number Diff line number Diff line change
@@ -108,6 +108,7 @@ export type DealerRecord = RecordMetadata & {
ArtPreviewCaption?: string;
IsAfterDark?: boolean;
Categories?: string[];
Keywords?: { [category: string]: string[] };
};

export type DealerDetails = DealerRecord & {

0 comments on commit 746dff7

Please sign in to comment.