diff --git a/apps/web/pages/events/index.tsx b/apps/web/pages/events/index.tsx index 8653312795e84efc5a201047ed788f211adc4bec..483e3dcca22de51afe8de0d4e85ace1cde7f30c4 100644 --- a/apps/web/pages/events/index.tsx +++ b/apps/web/pages/events/index.tsx @@ -1,7 +1,11 @@ import { EventList } from '@holi-apps/events/screens/EventList' -import type { NextPage } from 'next' -import React from 'react' +import React, { type ReactElement } from 'react' -const EventsPage: NextPage = () => <EventList /> +import { AppLayout } from '@holi/web/layout/NavigationLayoutVolunteeringApp' +import type { NextPageWithLayout } from '@holi/web/pages/_app' + +const EventsPage: NextPageWithLayout = () => <EventList /> + +EventsPage.getLayout = (page: ReactElement) => <AppLayout>{page}</AppLayout> export default EventsPage diff --git a/holi-apps/events/components/EventCarousel.tsx b/holi-apps/events/components/EventCarousel.tsx index c62cfe8f72cbe9dfd276dbe2ab7854cde035c7ea..ec05c4f7f44169ee15b48a0902b8c8617b64e09d 100644 --- a/holi-apps/events/components/EventCarousel.tsx +++ b/holi-apps/events/components/EventCarousel.tsx @@ -1,6 +1,6 @@ import React from 'react' import RecoSlider from '@holi/core/components/RecoSlider' -import { CheckboxFilled } from '@holi/icons/src/generated' +import { CalendarEventFilled } from '@holi/icons/src/generated' import { useTranslation } from 'react-i18next' import EventCard from '@holi-apps/events/components/EventCard' import { Stack } from 'holi-bricks/components/stack' @@ -37,7 +37,7 @@ const EventCarousel = ({ cardWidth }: EventCarouselProps) => { }} data={events} loading={user.isLoading} - icon={CheckboxFilled} + icon={CalendarEventFilled} heading={t('act.events.title')} subline={t('act.events.subline')} labelSeeMore={t('act.events.showAll')} diff --git a/holi-apps/events/components/EventsFilterForm.tsx b/holi-apps/events/components/EventsFilterForm.tsx index 5695aac3f3f66e321f673de3c1493440faf3e928..b7ecc2d2da347e1866f2f916847b0f69aff2514f 100644 --- a/holi-apps/events/components/EventsFilterForm.tsx +++ b/holi-apps/events/components/EventsFilterForm.tsx @@ -47,7 +47,15 @@ const EventsFilterForm = ({ onSubmit, filters }: EventFilterFormProps) => { <View style={styles.modalContent}> <Stack direction="row" alignItems="center" justifyContent="space-between" gap="3xs"> <Text size="3xl">{t('act.events.filter.heading')}</Text> - <Button label={t('global.reset')} inline onPress={resetForm} variant="secondary" size="sm" icon={Close} /> + <Button + disabled={localOnly} + label={t('global.reset')} + inline + onPress={resetForm} + variant="secondary" + size="sm" + icon={Close} + /> </Stack> <Stack direction="row" alignItems="center" justifyContent="space-between"> diff --git a/holi-apps/events/screens/EventList.tsx b/holi-apps/events/screens/EventList.tsx index 50dbf7d92441db57f90fce8784e94899795d6986..ca9505b96787c50cba512494cfecaf4742cdd4db 100644 --- a/holi-apps/events/screens/EventList.tsx +++ b/holi-apps/events/screens/EventList.tsx @@ -9,11 +9,8 @@ import HoliGridWrapper from '@holi/ui/components/atoms/HoliGridWrapper' import { Image } from 'holi-bricks/components/image' import HoliModal, { useModalState } from '@holi/ui/components/molecules/HoliModal' -import { createStyleSheet } from 'holi-bricks/utils' -import { useStyles } from 'holi-bricks/hooks' import { Text } from 'holi-bricks/components/text' import HeaderBackButton from '@holi/core/navigation/components/HeaderBackButton' -import HoliContainer from '@holi/ui/components/atoms/HoliContainer' import HoliButton from '@holi/ui/components/molecules/HoliButton' import EventsFilterForm, { type EventFilters } from '@holi-apps/events/components/EventsFilterForm' import EventCard from '@holi-apps/events/components/EventCard' @@ -32,9 +29,9 @@ const PAGE_SIZE_EXTERNAL_DATA = 10 export const EventList = () => { const { t } = useTranslation() - const { styles } = useStyles(stylesheet) const [fetchLocalOnlyEvents, setFetchLocalOnlyEvents] = useState(false) + const [isFiltering, setIsFiltering] = useState(false) const { events, canLoadMore, fetchMoreEvents } = useEventListData(PAGE_SIZE_EXTERNAL_DATA, fetchLocalOnlyEvents) const partner = EVENTS_SOURCE_MAPPING @@ -47,6 +44,7 @@ export const EventList = () => { const handleFilterChange = useCallback( async (filters: EventFilters) => { setFetchLocalOnlyEvents(filters.localOnly) + setIsFiltering(filters.localOnly) closeFilterModal() }, [closeFilterModal] @@ -59,6 +57,7 @@ export const EventList = () => { onPress={showFilterModal} testID="btn-to-volunteering-filter-modal" trackingEvent={TrackingEvent.Events.filter} + showBell={isFiltering} /> ) @@ -78,100 +77,75 @@ export const EventList = () => { headerTitleAlign: 'center', }} > - <HoliContainer> - <Stack padding={{ top: 'md' }}> - {events && events.length > 0 && ( - <Stack gap="xs"> - <Text headingLevel="2" size="3xl"> - {t('act.events.title')} - </Text> + <Stack padding={{ top: 'md' }}> + {events && events.length > 0 && ( + <Stack gap="xs"> + <Text headingLevel="2" size="3xl"> + {t('act.events.title')} + </Text> + <HoliButton transparent onPress={showPartnerModal} label={t('act.events.partnerInfoRowText')}> <Stack direction="row" alignItems="center" gap="3xs"> - <Stack borderRadius="pill"> - <View style={styles.imageContainer}> - <Image - source={require('@holi/ui/assets/img/events/gemeinschaftswerk-nachhaltigkeit-logo.png')} - width={33} - height={33} - alt="Logo Gemeinschaftswerk Nachhaltigkeit" - /> - </View> - </Stack> - <HoliButton transparent onPress={showPartnerModal} label={t('act.events.partnerInfoRowText')}> - <View style={{ flex: 3 }}> - <Text size="lg">{t('act.events.partnerInfoRowText')}</Text> - </View> - </HoliButton> + <Image + source={require('@holi/ui/assets/img/events/gemeinschaftswerk-nachhaltigkeit-logo.png')} + width={33} + height={33} + alt="Logo Gemeinschaftswerk Nachhaltigkeit" + /> + <View style={{ flex: 3 }}> + <Text size="lg">{t('act.events.partnerInfoRowText')}</Text> + </View> </Stack> + </HoliButton> - <Text size="lg" color="support"> - {events.length} {t('act.events.opportunities')} - </Text> + <Text size="lg" color="support"> + {events.length} {t('act.events.opportunities')} + </Text> - {/* info modal */} - <InfoModal - headline={t('volunteering:listing.partnerModal.headline')} - items={partner} - shown={partnerModalVisible} - onDismiss={closePartnerModal} - i18nPrefix={'act.events.listing.partnerModal'} - /> + {/* info modal */} + <InfoModal + headline={t('volunteering:listing.partnerModal.headline')} + items={partner} + shown={partnerModalVisible} + onDismiss={closePartnerModal} + i18nPrefix={'act.events.listing.partnerModal'} + /> - {/* filter modal */} - <HoliModal label={'Filter Events'} shown={filterModalVisible} onDismiss={closeFilterModal} autoHeight> - <EventsFilterForm - filters={{ - localOnly: fetchLocalOnlyEvents, - }} - onSubmit={handleFilterChange} - /> - </HoliModal> - </Stack> - )} - <HoliBox padding={[24, 0, 24, 0]}> - {/* <View testID={isFiltering ? 'volunteering-list-filtered' : 'volunteering-list-unfiltered'}> */} - <HoliGridWrapper> - {Array.isArray(events) && - events.map((event) => { - return ( - <HoliGridCell key={event.id}> - <EventCard event={event} /> - </HoliGridCell> - ) - })} - </HoliGridWrapper> - {/* </View> */} - </HoliBox> - {canLoadMore && ( - <Stack padding={{ bottom: 'md' }}> - <ButtonTracked - label={t('global.seeMore')} - loadingLabel={t('global.loading')} - inline - onPress={fetchMoreEvents} - testID="btn-fetch-more" - trackingEvent={TrackingEvent.Events.loadMore} + {/* filter modal */} + <HoliModal label={'Filter Events'} shown={filterModalVisible} onDismiss={closeFilterModal} autoHeight> + <EventsFilterForm + filters={{ + localOnly: fetchLocalOnlyEvents, + }} + onSubmit={handleFilterChange} /> - </Stack> - )} - </Stack> - </HoliContainer> + </HoliModal> + </Stack> + )} + <HoliBox padding={[24, 0, 24, 0]}> + <HoliGridWrapper gap={8}> + {Array.isArray(events) && + events.map((event) => { + return ( + <HoliGridCell key={event.id}> + <EventCard event={event} /> + </HoliGridCell> + ) + })} + </HoliGridWrapper> + </HoliBox> + {canLoadMore && ( + <Stack padding={{ bottom: 'md' }}> + <ButtonTracked + label={t('global.seeMore')} + loadingLabel={t('global.loading')} + inline + onPress={fetchMoreEvents} + testID="btn-fetch-more" + trackingEvent={TrackingEvent.Events.loadMore} + /> + </Stack> + )} + </Stack> </Screen> ) } - -const stylesheet = createStyleSheet((theme) => ({ - defaultLayout: { - backgroundColor: theme.colors.bg.page, - paddingBottom: 0, - }, - imageContainer: { - borderColor: theme.colors.border.default, - overflow: 'hidden', - display: 'flex', - justifyContent: 'center', - justifySelf: 'center', - alignContent: 'center', - alignSelf: 'center', - textAlign: 'center', - }, -})) diff --git a/holi-bricks/components/card/CompactCard.tsx b/holi-bricks/components/card/CompactCard.tsx index 8d64b4db9fd83f56dd6fa9c18ecb9189cfc4dc22..937ed178bbe88d85742c4a7f4652eae74bbe2fe3 100644 --- a/holi-bricks/components/card/CompactCard.tsx +++ b/holi-bricks/components/card/CompactCard.tsx @@ -55,7 +55,6 @@ export const CompactCard = ({ require('@holi/ui/assets/img/events/thumbnail-yellow-full.svg'), ] const randomPlaceholderImage = useRef(placeholderImages[Math.floor(Math.random() * placeholderImages.length)]).current - return ( <View style={styles.cardContainer(maxWidth)}> <Stack borderWidth={1} borderColor="support" borderStyle="solid" borderRadius="md"> @@ -70,6 +69,7 @@ export const CompactCard = ({ width={90} alt={t('act.events.thumbnail.label')} onError={handleImageError} + whiteOverlay /> </View> diff --git a/holi-bricks/components/image/Image.tsx b/holi-bricks/components/image/Image.tsx index 1e912184d2a1df7b9f8176b726521b54ce9925ac..87f34356ac69304f3f3f05658033caf0bf4c05e5 100644 --- a/holi-bricks/components/image/Image.tsx +++ b/holi-bricks/components/image/Image.tsx @@ -1,11 +1,13 @@ import type React from 'react' import { useState } from 'react' -import { Image as RNImage } from 'react-native' +import { Image as RNImage, View } from 'react-native' import { Image as ExpoImage } from 'expo-image' //import { WithLocalSvg } from 'react-native-svg/css' import { accessibilityProps } from 'holi-bricks/accessibility' import type { ImageProps } from 'holi-bricks/components/image/type' import { setScalingParams } from 'holi-bricks/components/image/helpers' +import { createStyleSheet } from 'holi-bricks/utils' +import { useStyles } from 'holi-bricks/hooks' const isLocalSvg = (source: number) => { try { @@ -32,12 +34,14 @@ export const Image: React.FC<ImageProps> = ({ fallbackSource, blurhash, alt, + whiteOverlay: overlay, ...otherProps }) => { const [hasError, setHasError] = useState(false) const transformedSource = setScalingParams(source, width, height) const isRemote = isRemoteSource(source) const localSvg = typeof source === 'number' && isLocalSvg(source) + const { styles } = useStyles(stylesheet) if (!source) return null @@ -59,25 +63,39 @@ export const Image: React.FC<ImageProps> = ({ } return ( - <ExpoImage - source={hasError && fallbackSource ? fallbackSource : transformedSource} - alt={alt} - role="img" - accessibilityLabel={alt} - accessibilityRole={otherProps?.['aria-hidden'] === true ? 'none' : 'image'} - accessible={otherProps?.['aria-hidden'] === true} - placeholder={isRemote ? blurhash : undefined} - style={[{ width, height }]} - contentFit={resizeMode} - onError={() => { - setHasError(true) - // eslint-disable-next-line no-console - console.warn(`[Image] Failed to load image: ${source}`) - if (onError) onError() - }} - aria-label={alt} - onLoadEnd={onLoadEnd} - {...accessibilityProps(otherProps)} - /> + <> + <ExpoImage + source={hasError && fallbackSource ? fallbackSource : transformedSource} + alt={alt} + role="img" + accessibilityLabel={alt} + accessibilityRole={otherProps?.['aria-hidden'] === true ? 'none' : 'image'} + accessible={otherProps?.['aria-hidden'] === true} + placeholder={isRemote ? blurhash : undefined} + style={[{ width, height }]} + contentFit={resizeMode} + onError={() => { + setHasError(true) + // eslint-disable-next-line no-console + console.warn(`[Image] Failed to load image: ${source}`) + if (onError) onError() + }} + aria-label={alt} + onLoadEnd={onLoadEnd} + {...accessibilityProps(otherProps)} + /> + {overlay && <View style={styles.overlay} />} + </> ) } + +const stylesheet = createStyleSheet({ + overlay: { + position: 'absolute', + top: 0, + left: 0, + width: '100%', + height: '100%', + backgroundColor: 'rgba(255, 255, 255, 0.2)', + }, +}) diff --git a/holi-bricks/components/image/Image.web.tsx b/holi-bricks/components/image/Image.web.tsx index fc0b18b1e814ddedc4d05f7c9d2f0cff7baf4714..9be9ad7440da35b4616b50783a59e8c9a183454d 100644 --- a/holi-bricks/components/image/Image.web.tsx +++ b/holi-bricks/components/image/Image.web.tsx @@ -4,6 +4,9 @@ import { useState } from 'react' import NextImage from 'next/image' import { accessibilityProps } from 'holi-bricks/accessibility' import { setScalingParams } from 'holi-bricks/components/image/helpers' +import { createStyleSheet } from 'holi-bricks/utils' +import { useStyles } from 'holi-bricks/hooks' +import { View } from 'react-native' export const isRemoteSource = (source: string): source is string => { return /^(https?:)?\/\//i.test(source) @@ -19,30 +22,46 @@ export const Image: React.FC<ImageProps> = ({ onLoadEnd, fallbackSource, blurhash, + whiteOverlay: overlay, ...otherProps }) => { const [hasError, setHasError] = useState(false) const isRemote = isRemoteSource(source as string) const transformedSource = (isRemote ? setScalingParams(source as string, width, height) : source) as string + const { styles } = useStyles(stylesheet) return ( - <NextImage - src={hasError && fallbackSource ? (fallbackSource as string) : transformedSource} - alt={alt} - role="img" - aria-label={alt} - blurDataURL={isRemote ? blurhash : undefined} - width={width} - height={height} - style={{ objectFit: resizeMode }} - onError={() => { - setHasError(true) - // eslint-disable-next-line no-console - console.warn(`[Image] Failed to load image: ${transformedSource}`) - if (onError) onError() - }} - onLoad={onLoadEnd} - {...accessibilityProps(otherProps)} - /> + <> + <NextImage + src={hasError && fallbackSource ? (fallbackSource as string) : transformedSource} + alt={alt} + role="img" + aria-label={alt} + blurDataURL={isRemote ? blurhash : undefined} + width={width} + height={height} + style={{ objectFit: resizeMode }} + onError={() => { + setHasError(true) + // eslint-disable-next-line no-console + console.warn(`[Image] Failed to load image: ${transformedSource}`) + if (onError) onError() + }} + onLoad={onLoadEnd} + {...accessibilityProps(otherProps)} + /> + {overlay && <View style={styles.overlay} />} + </> ) } + +const stylesheet = createStyleSheet({ + overlay: { + position: 'absolute', + top: 0, + left: 0, + width: '100%', + height: '100%', + backgroundColor: 'rgba(255, 255, 255, 0.2)', + }, +}) diff --git a/holi-bricks/components/image/type.ts b/holi-bricks/components/image/type.ts index 322c58b0c680dccedd0932e749cede7250284b7b..861a3196961b7568e74efae6da917596121f5019 100644 --- a/holi-bricks/components/image/type.ts +++ b/holi-bricks/components/image/type.ts @@ -13,4 +13,5 @@ export interface ImageProps extends AccessibilityProps { fallbackSource?: string | number blurhash?: string alt: string + whiteOverlay?: boolean }