Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Customize activitybar #1009

Merged
merged 10 commits into from
Jan 8, 2024
15 changes: 15 additions & 0 deletions docs/customization.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,21 @@ In json files, you can just override the primary keys you need. You have to over

- `home.json` to define homepage settings.

- `activityBar`: This is a menu showing all search types (hiking practices, outdoor practices, tourist content categories and tourist events categories).

- `shouldDisplay`: Boolean allowing this menu to be displayed or not. Its default value is `true`.
- `numberOfItemsBeforeTruncation` The number of items displayed on the screen. To see the others, click on the "Show more" button. Its default value is `8`.
- `links`: Allows you to customize the order and display of categories links. It's an array containing an object with 3 properties:
```typescript
{
"type" : 'trek' | 'outdoorSite' | 'touristicContent' | 'touristicEvent' ;
"grouped" : boolean ; // If set to "true", all activities of the type are grouped under a single link.
"iconUrl" : string ; // Optional, url to replace default icon. Used only if "grouped" is set to "true",
}
```

More explanations in this [comments](https://github.com/GeotrekCE/Geotrek-rando-v3/issues/560#issuecomment-1858166341) (in French).

- `suggestions`: You can define blocks to display suggestions groups with treks ID, outdoor sites ID, services ID or events ID to highlight on homepage (see https://github.com/GeotrekCE/Geotrek-rando-v3/blob/main/frontend/customization/config/home.json).
Each group has the following properties :

Expand Down
19 changes: 18 additions & 1 deletion frontend/config/home.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,24 @@
"shouldDisplayText": true
},
"activityBar": {
"shouldDisplay": true
"shouldDisplay": true,
"numberOfItemsBeforeTruncation": 8,
"links": [{
"type": "trek",
"grouped": false
},
{
"type": "outdoorSite",
"grouped": false
},
{
"type": "touristicContent",
"grouped": false
},
{
"type": "touristicEvent",
"grouped": false
}]
},
"suggestions": {
"default": []
Expand Down
19 changes: 18 additions & 1 deletion frontend/customization/config/home.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,24 @@
"shouldDisplayText": true
},
"activityBar": {
"shouldDisplay": true
"shouldDisplay": true,
"numberOfItemsBeforeTruncation": 8,
"links": [{
"type": "trek",
"grouped": false
},
{
"type": "outdoorSite",
"grouped": false
},
{
"type": "touristicContent",
"grouped": false
},
{
"type": "touristicEvent",
"grouped": false
}]
},
"suggestions": {
"default": [
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,55 @@
import React from 'react';
import SVG from 'react-inlinesvg';
import getConfig from 'next/config';
import styled from 'styled-components';

import { optimizeAndDefineColor } from 'stylesheet';
import { Link } from 'components/Link';
import getActivityColor from 'components/pages/search/components/ResultCard/getActivityColor';
import { ActivityFilter } from 'modules/activities/interface';

interface Props {
iconUrl: string;
href: string;
label: string;
type: ActivityFilter['type'];
}

export const ActivityButton: React.FC<Props> = ({ iconUrl, href, label }) => {
const {
publicRuntimeConfig: { colors },
} = getConfig();

const getColor = (type: ActivityFilter['type']) => {
if (type === 'PRACTICE') {
return getActivityColor('practices');
}
if (type === 'OUTDOOR_PRACTICE') {
return getActivityColor('outdoorPractice');
}
if (type === 'CATEGORY') {
return getActivityColor('categories');
}
if (type === 'TOURISTIC_EVENT_TYPE') {
return getActivityColor('event');
}
return colors.primary3;
};

export const ActivityButton: React.FC<Props> = ({ iconUrl, href, label, type }) => {
return (
<Link
<StyleLink
$color={getColor(type)}
href={href}
className="flex flex-col items-center text-center mt-6 text-greyDarkColored bg-white transition hover:text-primary3 focus:text-primary3"
className={`flex flex-col items-center text-center mt-6 text-greyDarkColored bg-white transition`}
>
<SVG src={iconUrl} className="h-9 desktop:w-12" preProcessor={optimizeAndDefineColor()} />
<span className="w-20 text-sm mt-2 text-ellipsis overflow-hidden">{label}</span>
</Link>
</StyleLink>
);
};

const StyleLink = styled(Link)<{ $color?: string }>`
&:hover,
&:focus {
color: ${props => props.$color};
}
`;
102 changes: 54 additions & 48 deletions frontend/src/components/ActivitySearchFilter/ActivitySearchFilter.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,26 @@
import { ChevronDown } from 'components/Icons/ChevronDown';
import { MoreHorizontal } from 'components/Icons/MoreHorizontal';
import React from 'react';
import { FormattedMessage } from 'react-intl';
import { FormattedMessage, useIntl } from 'react-intl';
import { routes } from 'services/routes';
import { ActivityFilter } from 'modules/activities/interface';
import { CATEGORY_ID, EVENT_ID, OUTDOOR_ID, PRACTICE_ID } from 'modules/filters/constant';

import { cn } from 'services/utils/cn';
import { ActivityButton } from './ActivityButton';
import { useActivitySearchFilter } from './useActivitySearchFilter';
import { ActivitySearchFilterMobile } from './ActivitySearchFilterMobile';

interface Props {
className?: string;
itemsToDisplayBeforeTruncation: number;
}

const MAX_VISIBLE_ACTIVITIES = 8;

export const ActivitySearchFilter: React.FC<Props> = ({ className }) => {
export const ActivitySearchFilter: React.FC<Props> = ({
className,
itemsToDisplayBeforeTruncation = 8,
}) => {
const { activities, expandedState, toggleExpandedState } = useActivitySearchFilter();

const collapseIsNeeded: boolean =
activities !== undefined && activities.length > MAX_VISIBLE_ACTIVITIES;

const visibleActivities: ActivityFilter[] | undefined =
activities !== undefined
? collapseIsNeeded && expandedState === 'COLLAPSED'
? activities.slice(0, MAX_VISIBLE_ACTIVITIES)
: activities
: undefined;
const intl = useIntl();

const getId = (type: string) => {
if (type === 'PRACTICE') return PRACTICE_ID;
Expand All @@ -37,41 +30,54 @@ export const ActivitySearchFilter: React.FC<Props> = ({ className }) => {
return CATEGORY_ID;
};

if (!activities) {
return null;
}

const collapseIsNeeded: boolean = activities.length > itemsToDisplayBeforeTruncation;

const visibleActivities: ActivityFilter[] =
collapseIsNeeded && expandedState === 'COLLAPSED'
? activities.slice(0, itemsToDisplayBeforeTruncation)
: activities;

return (
<div>
{activities !== undefined && (
<>
<div
className={`px-3 pb-6 bg-white shadow-lg rounded-2xl hidden self-center max-w-activitySearchFilter desktop:flex${
className ?? ''
}`}
<nav role="navigation">
<div
className={cn(
'px-3 pb-6 bg-white shadow-lg rounded-2xl hidden self-center max-w-activitySearchFilter desktop:flex',
className,
)}
>
<div className="flex content-evenly flex-wrap flex-1 items-center">
{visibleActivities.map(activity => (
<ActivityButton
iconUrl={activity.pictogramUri}
href={`${routes.SEARCH}?${getId(activity.type)}=${activity.id}`}
key={`${activity.type}-${activity.id}`}
label={
activity.titleTranslationId
? intl.formatMessage({ id: activity.titleTranslationId })
: activity.label
}
type={activity.type}
/>
))}
</div>
{collapseIsNeeded && (
<button
type="button"
className="self-end hover:text-primary3 transition-colors text-greyDarkColored"
onClick={toggleExpandedState}
>
<div className="flex content-evenly flex-wrap flex-1 items-center">
{visibleActivities?.map(activity => (
<ActivityButton
iconUrl={activity.pictogramUri}
href={`${routes.SEARCH}?${getId(activity.type)}=${activity.id}`}
key={`${activity.type}-${activity.id}`}
label={activity.label}
/>
))}
</div>
{collapseIsNeeded && (
<button
type="button"
className="self-end hover:text-primary3 transition-colors text-greyDarkColored"
onClick={toggleExpandedState}
>
<ControlCollapseButton expandedState={expandedState} />
</button>
)}
</div>
<div className="block desktop:hidden">
<ActivitySearchFilterMobile activities={activities ?? []} getId={getId} />
</div>
</>
)}
</div>
<ControlCollapseButton expandedState={expandedState} />
</button>
)}
</div>
<div className="block desktop:hidden">
<ActivitySearchFilterMobile activities={activities} getId={getId} />
</div>
</nav>
);
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import Select, { CSSObjectWithLabel } from 'react-select';
import { FormattedMessage } from 'react-intl';
import { FormattedMessage, useIntl } from 'react-intl';

import { colorPalette, getSpacing, shadow } from 'stylesheet';
import { routes } from 'services/routes';
Expand All @@ -17,6 +17,7 @@ export const ActivitySearchFilterMobile: React.FC<{
getId: (type: string) => string;
}> = ({ className = '', activities, getId }) => {
const { selectedActivityId, updateSelectedActivityId } = useActivitySearchFilterMobile();
const intl = useIntl();

const selectedActivity = activities.find(
({ id, type }) => `${type}-${id}` === selectedActivityId,
Expand All @@ -26,9 +27,9 @@ export const ActivitySearchFilterMobile: React.FC<{
<div className={`${className} flex space-x-4 items-center`}>
<Select
className="flex-1"
options={activities.map(({ id, label, type }) => ({
options={activities.map(({ id, label, titleTranslationId, type }) => ({
value: `${type}-${id}`,
label,
label: titleTranslationId ? intl.formatMessage({ id: titleTranslationId }) : label,
}))}
styles={selectStyles}
instanceId="activitySearchFilterMobile"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@ import { useQuery } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import { getDefaultLanguage } from 'modules/header/utills';
import { getActivityBarContent } from 'modules/activities/connector';
import { getHomePageConfig } from 'modules/home/utils';

const { activityBar } = getHomePageConfig();

export const useActivitySearchFilter = () => {
const language = useRouter().locale ?? getDefaultLanguage();
const { data: activities } = useQuery<ActivityFilter[], Error>(['homeActivities', language], () =>
getActivityBarContent(language),
getActivityBarContent(language, activityBar.links),
);

const [expandedState, setExpandedState] = useState<'EXPANDED' | 'COLLAPSED'>('COLLAPSED');
Expand Down
20 changes: 10 additions & 10 deletions frontend/src/components/pages/home/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,13 @@ const {
} = getNextConfig();

const HomeUI: React.FC = () => {
const { config, suggestions } = useHome();
const {
config: { activityBar, welcomeBanner },
suggestions,
} = useHome();

const contentContainerClassname = `relative ${
config.activityBar.shouldDisplay ? '-top-6 desktop:-top-15' : 'pt-6 desktop:pt-18'
activityBar.shouldDisplay ? '-top-6 desktop:-top-15' : 'pt-6 desktop:pt-18'
}`;

const intl = useIntl();
Expand All @@ -32,19 +35,16 @@ const HomeUI: React.FC = () => {
description={intl.formatMessage({ id: 'home.description' })}
/>
<HomeContainer id="home_container">
<BannerWithAsset
shouldDisplayText={config.welcomeBanner.shouldDisplayText}
carouselUrls={config.welcomeBanner.carouselUrls}
pictureUrl={config.welcomeBanner.pictureUrl}
videoUrl={config.welcomeBanner.videoUrl}
/>
<BannerWithAsset {...welcomeBanner} />
<div id="home_content" className={contentContainerClassname}>
{config.activityBar.shouldDisplay && (
{activityBar.shouldDisplay && (
<div
className={`desktop:flex desktop:justify-center ${classNameHomeChild}`}
id="home_activitiesBar"
>
<ActivitySearchFilter />
<ActivitySearchFilter
itemsToDisplayBeforeTruncation={activityBar.numberOfItemsBeforeTruncation}
/>
</div>
)}
{homeTop !== undefined && (
Expand Down
23 changes: 21 additions & 2 deletions frontend/src/modules/activities/adapter.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { PRACTICE_ID } from 'modules/filters/constant';
import { FilterWithoutType } from 'modules/filters/interface';
import { sortedByOrder } from 'modules/utils/array';
import { ActivityBarLinks } from 'modules/home/interface';
import { Activity, ActivityChoices, ActivityFilter, RawListActivity } from './interface';

const isCompleteRawListActivity = (
Expand Down Expand Up @@ -47,8 +48,25 @@ export const adaptActivities = (rawActivities: Partial<RawListActivity>[]): Acti

export const adaptActivitiesFilter = (
rawActivities: Partial<RawListActivity>[],
): ActivityFilter[] =>
rawActivities
{ grouped, iconUrl }: Partial<ActivityBarLinks>,
): ActivityFilter[] => {
if (grouped) {
return [
{
label: 'Practices',
titleTranslationId: 'home.activityBar.practices',
pictogramUri: iconUrl ?? '/icons/practice-trek.svg',
id: rawActivities
.filter(isCompleteRawListActivity)
.map(({ id }) => `${id}`)
.sort()
.join(','),
order: null,
type: 'PRACTICE',
},
];
}
return rawActivities
.filter(isCompleteRawListActivity)
.sort(sortedByOrder)
.map(({ name, pictogram, id, order = null }) => ({
Expand All @@ -58,3 +76,4 @@ export const adaptActivitiesFilter = (
order,
type: 'PRACTICE',
}));
};
Loading
Loading