Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • fcos-suite/fcos-suite-map
1 result
Show changes
Showing
with 510 additions and 102 deletions
...@@ -2,9 +2,7 @@ import React from 'react'; ...@@ -2,9 +2,7 @@ import React from 'react';
import { CheckCircleOutline as CheckIcon, X as XIcon, ExclamationOutline as AlertIcon } from 'heroicons-react'; import { CheckCircleOutline as CheckIcon, X as XIcon, ExclamationOutline as AlertIcon } from 'heroicons-react';
import { useStore } from '../hooks'; import { useStore } from '../hooks';
interface Props {} const Notification: React.FC = () => {
const Notification: React.FC<Props> = () => {
const notification = useStore((state) => state.notification); const notification = useStore((state) => state.notification);
const setNotification = useStore((state) => state.setNotification); const setNotification = useStore((state) => state.setNotification);
......
import React, { useEffect } from 'react';
import { usePoiData, useStore } from '../hooks';
interface Props {
poiId: string | null;
}
const PoiLoader: React.FC<Props> = ({ poiId }) => {
const { data } = usePoiData();
const setSelectedPoi = useStore((state) => state.setSelectedPoi);
useEffect(() => {
if (data && poiId !== undefined) {
const poi = poiId ? data.find((poi) => String(poi.id) === poiId) : null;
if (poi !== undefined) setSelectedPoi(poi);
}
}, [data, poiId]);
return <></>;
};
export default PoiLoader;
import React from 'react';
import type { Theme } from 'react-select';
import ReactSelect, { NamedProps, StylesConfig, components } from 'react-select';
import Tag from './Tag';
const Option = ({ children, data, ...rest }: { children: any; data: any }) => {
return (
// @ts-expect-error: Not typed yet
<components.Option {...rest}>
<Tag color={data.value.color}>{children}</Tag>
</components.Option>
);
};
const Select = <OptionType, isMulti extends boolean>(props: NamedProps<OptionType, isMulti>): JSX.Element => {
const customStyles: StylesConfig<OptionType, isMulti> = {
control: (provided) => ({ ...provided, border: '0', borderRadius: '0.5em' }),
multiValue: (provided, state) => ({
...provided,
// @ts-expect-error: Not typed yet
backgroundColor: state?.data?.value?.color || 'grey',
borderRadius: '999px',
padding: '0 3px',
}),
multiValueRemove: (provided) => ({
...provided,
color: 'hsl(0, 0%, 50%)',
'&:hover': { backgroundColor: 'initial', color: 'black' },
}),
};
return (
<ReactSelect
components={{
Option,
}}
theme={(theme): Theme => ({
...theme,
// @ts-expect-error: ThemeConfig type from definitely-typed is not complete
borderRadius: '0.5em',
colors: {
...theme.colors,
primary25: '#C7D2FE',
},
})}
placeholder={'Tags auswählen...'}
styles={customStyles}
name="pois"
className="hover:border-opacity-40 rounded-lg w-full border-2 border-black border-opacity-20 focus-within:border-indigo-300 focus-within:ring focus-within:ring-indigo-200 focus-within:ring-opacity-50 mt-1"
{...props}
/>
);
};
export default Select;
import React, { SyntheticEvent } from 'react';
import { X as CloseIcon } from 'heroicons-react';
interface Props {
onClick: (event: SyntheticEvent) => void;
absolute?: boolean;
}
const CloseButton: React.FC<Props> = ({ onClick, absolute = false }) => {
return (
<CloseIcon
size={32}
className={`${
absolute ? 'absolute left-5 top-5' : ''
} p-1 text-gray-600 inline-block cursor-pointer bg-gray-200 bg-opacity-30 hover:bg-opacity-80 rounded-full`}
onClick={onClick}
/>
);
};
export default CloseButton;
...@@ -16,7 +16,7 @@ const ListElement: React.FC<Props> = ({ value, hovered, ...restProps }) => { ...@@ -16,7 +16,7 @@ const ListElement: React.FC<Props> = ({ value, hovered, ...restProps }) => {
{...restProps} {...restProps}
className={`border-2 border-black border-opacity-20 ${ className={`border-2 border-black border-opacity-20 ${
hovered ? 'border-opacity-40' : 'hover:border-opacity-40' hovered ? 'border-opacity-40' : 'hover:border-opacity-40'
} cursor-pointer rounded-lg md:overflow-hidden mx-4 my-2`} } cursor-pointer rounded-lg mx-4 my-2`}
> >
<div className="p-3"> <div className="p-3">
<h2 className="tracking-widest text-xs uppercase title-font font-medium text-gray-400 mb-1"> <h2 className="tracking-widest text-xs uppercase title-font font-medium text-gray-400 mb-1">
......
import { X as CloseIcon } from 'heroicons-react';
import React from 'react'; import React from 'react';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import AddPoiForm from '../Form/AddPoiForm'; import AddPoiForm from '../Form/AddPoiForm';
import CloseButton from './CloseButton';
import SidebarContainer from './SidebarContainer'; import SidebarContainer from './SidebarContainer';
interface Props {} const SidebarCreateView: React.FC = () => {
const SidebarCreateView: React.FC<Props> = () => {
const history = useHistory(); const history = useHistory();
return ( return (
<SidebarContainer className="p-5"> <SidebarContainer className="p-5">
<CloseIcon <CloseButton onClick={() => history.push('/')} />
size={32}
className={`flex-shrink-0 left-5 top-5 p-1 text-gray-500 inline-block cursor-pointer hover:bg-gray-300 hover:bg-opacity-50 rounded-full`}
onClick={() => history.push('/')}
/>
<h1 className="text-xl font-medium title-font text-gray-900 mt-2 mb-4">Neuen Ort anlegen:</h1> <h1 className="text-xl font-medium title-font text-gray-900 mt-2 mb-4">Neuen Ort anlegen:</h1>
<AddPoiForm /> <AddPoiForm />
</SidebarContainer> </SidebarContainer>
......
import React from 'react'; import React, { useState } from 'react';
import { usePoiData, useStore } from '../../hooks'; import { usePoiData, useStore } from '../../hooks';
import ListElement from './ListElement'; import ListElement from './ListElement';
import SidebarContainer from './SidebarContainer'; import SidebarContainer from './SidebarContainer';
import { removeDuplicateObjects } from '../../util/array';
interface Props {} import { useFilteredPoiData } from '../../hooks/useFilteredPoiData';
import type { Tag } from '../../types/PointOfInterest';
const SidebarListView: React.FC<Props> = () => { import { FilterOutline as FilterIcon } from 'heroicons-react';
const { data } = usePoiData(); import { useHistory } from 'react-router-dom';
const hoveredPoi = useStore((state) => state.hoveredPoi); import Select from '../Select';
const setHoveredPoi = useStore((state) => state.setHoveredPoi);
const setSelectedPoi = useStore((state) => state.setSelectedPoi); const SidebarListView: React.FC = () => {
const tagsToSelectOptions = (tags?: Tag[]) => tags?.map((tag) => ({ label: tag.displayName, value: tag }));
return ( const history = useHistory();
<SidebarContainer>
<h1 className="text-xl font-medium title-font m-4 text-gray-900 mb-2">{data?.length} Orte:</h1> const { data } = usePoiData();
{data?.map((poi) => ( const { data: filteredData, filterTags, setFilterTags } = useFilteredPoiData();
<ListElement const hoveredPoi = useStore((state) => state.hoveredPoi);
key={poi.id} const setHoveredPoi = useStore((state) => state.setHoveredPoi);
onMouseEnter={() => setHoveredPoi(poi)} const [filterInputIsOpen, setFilterInputIsOpen] = useState(false);
onMouseLeave={() => setHoveredPoi(null)}
onClick={() => setSelectedPoi(poi)} const tags =
value={poi} data &&
hovered={hoveredPoi?.id === poi.id} removeDuplicateObjects(
/> data?.flatMap((poi) => poi.tags),
))} 'id',
</SidebarContainer> );
); const options = tagsToSelectOptions(tags);
};
return (
export default SidebarListView; <SidebarContainer>
<div className="flex-col m-4 mb-2 pb-2 border-black border-opacity-20 border-b-2">
<div className="flex justify-between items-center">
<h1 className="text-xl font-medium title-font text-gray-900">{filteredData?.length} Orte:</h1>
<FilterIcon
onClick={() => setFilterInputIsOpen(!filterInputIsOpen)}
className="text-black text-opacity-20 hover:text-opacity-60 w-5 h-5 cursor-pointer"
/>
</div>
{filterInputIsOpen && (
<div className="py-2">
<Select
options={options}
isMulti={true}
value={filterTags && tagsToSelectOptions(filterTags)}
onChange={(selectedOptions) => setFilterTags(selectedOptions.map((opt) => opt.value))}
/>
</div>
)}
</div>
{filteredData?.map((poi) => (
<ListElement
key={poi.id}
onMouseEnter={() => setHoveredPoi(poi)}
onMouseLeave={() => setHoveredPoi(null)}
onClick={() => {
history.push(`/poi/${String(poi.id)}`);
}}
value={poi}
hovered={hoveredPoi?.id === poi.id}
/>
))}
</SidebarContainer>
);
};
export default SidebarListView;
import { HomeOutline as HomeIcon, LocationMarkerOutline as AddressIcon, X as CloseIcon } from 'heroicons-react'; import {
HomeOutline as HomeIcon,
LocationMarkerOutline as AddressIcon,
UserGroupOutline as RealtionStatusIcon,
} from 'heroicons-react';
import React from 'react'; import React from 'react';
import { useStore } from '../../hooks'; import { useStore } from '../../hooks';
import SidebarContainer from './SidebarContainer'; import SidebarContainer from './SidebarContainer';
import Tag from '../Tag'; import Tag from '../Tag';
import CloseButton from './CloseButton';
import { useHistory } from 'react-router-dom';
interface Props {} const SidebarSingleView: React.FC = () => {
const SidebarSingleView: React.FC<Props> = () => {
const selectedPoi = useStore((state) => state.selectedPoi); const selectedPoi = useStore((state) => state.selectedPoi);
const setSelectedPoi = useStore((state) => state.setSelectedPoi);
const strippedUrl = selectedPoi?.website?.replace(/(^\w+:|^)\/\//, ''); const strippedUrl = selectedPoi?.website?.replace(/(^\w+:|^)\/\//, '');
const history = useHistory();
return ( return (
<SidebarContainer className={`relative p-0`}> <SidebarContainer className={`relative p-0`}>
<div className={`${selectedPoi?.image ? '' : 'pl-5 pt-5 '}`}> <div className={`${selectedPoi?.image ? '' : 'pl-5 pt-5 '}`}>
<CloseIcon <CloseButton
size={32} absolute
className={`${ onClick={() => {
selectedPoi?.image ? 'absolute left-5 top-5 ' : '' history.push('/');
}p-1 text-gray-500 inline-block cursor-pointer hover:bg-gray-300 hover:bg-opacity-50 rounded-full`} }}
onClick={() => setSelectedPoi(null)}
/> />
</div> </div>
{selectedPoi?.image && ( {selectedPoi?.image && (
...@@ -38,7 +41,12 @@ const SidebarSingleView: React.FC<Props> = () => { ...@@ -38,7 +41,12 @@ const SidebarSingleView: React.FC<Props> = () => {
{selectedPoi?.website && ( {selectedPoi?.website && (
<div className={'flex items-center'}> <div className={'flex items-center'}>
<HomeIcon size={18} className={'text-gray-500 mr-2'} /> <HomeIcon size={18} className={'text-gray-500 mr-2'} />
<a className={'text-sm text-gray-500 hover:underline'} href={selectedPoi?.website}> <a
target="_blank"
rel="noopener noreferrer"
className={'text-sm text-gray-500 hover:underline'}
href={selectedPoi?.website}
>
{strippedUrl} {strippedUrl}
</a> </a>
</div> </div>
...@@ -49,6 +57,12 @@ const SidebarSingleView: React.FC<Props> = () => { ...@@ -49,6 +57,12 @@ const SidebarSingleView: React.FC<Props> = () => {
<div className="text-sm text-gray-500">{selectedPoi?.address}</div> <div className="text-sm text-gray-500">{selectedPoi?.address}</div>
</div> </div>
)} )}
{selectedPoi?.relationStatus && (
<div className={'flex items-center mt-3'}>
<RealtionStatusIcon size={18} className={'text-gray-500 mr-2'} />
<div className="text-sm text-gray-500">{selectedPoi?.relationStatus}</div>
</div>
)}
{!!selectedPoi?.tags?.length && ( {!!selectedPoi?.tags?.length && (
<div className={'flex items-center mt-3 flex-wrap'}> <div className={'flex items-center mt-3 flex-wrap'}>
{selectedPoi?.tags.map((tag) => ( {selectedPoi?.tags.map((tag) => (
......
import React from 'react'; import React from 'react';
interface Props {} const Spinner: React.FC = () => {
const Spinner: React.FC<Props> = () => {
return ( return (
<svg <svg
className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" className="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
......
...@@ -3,9 +3,7 @@ import { SWRConfig } from 'swr'; ...@@ -3,9 +3,7 @@ import { SWRConfig } from 'swr';
import { useStore } from '../hooks'; import { useStore } from '../hooks';
import fetcher from '../util/fetcher'; import fetcher from '../util/fetcher';
interface Props {} const SwrWrapper: React.FC = ({ children }) => {
const SwrWrapper: React.FC<Props> = ({ children }) => {
const setError = useStore((state) => state.setError); const setError = useStore((state) => state.setError);
const error = useStore((state) => state.error); const error = useStore((state) => state.error);
return ( return (
......
import type { GraphQLClient } from 'graphql-request';
import type * as Dom from 'graphql-request/dist/types.dom';
import gql from 'graphql-tag';
export type Maybe<T> = T | null;
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
ID: string;
String: string;
Boolean: boolean;
Int: number;
Float: number;
/** A date string with format `Y-m-d`, e.g. `2011-05-23`. */
Date: any;
/** A datetime string with format `Y-m-d H:i:s`, e.g. `2018-05-23 13:43:32`. */
DateTime: any;
/** Can be used as an argument to upload files using https://github.com/jaydenseric/graphql-multipart-request-spec */
Upload: any;
};
export type Mutation = {
__typename?: 'Mutation';
createPoi?: Maybe<Scalars['Boolean']>;
createTags?: Maybe<Array<Maybe<Tag>>>;
};
export type MutationCreatePoiArgs = {
poi: PoiInput;
};
export type MutationCreateTagsArgs = {
tags: Array<TagInput>;
};
export type Poi = {
__typename?: 'POI';
id: Scalars['ID'];
lat: Scalars['Float'];
lng: Scalars['Float'];
name: Scalars['String'];
published: Scalars['Boolean'];
website?: Maybe<Scalars['String']>;
description?: Maybe<Scalars['String']>;
address: Scalars['String'];
image: Scalars['String'];
category: Scalars['String'];
relationStatus: Scalars['String'];
tags?: Maybe<Array<Maybe<Tag>>>;
created_at?: Maybe<Scalars['DateTime']>;
updated_at?: Maybe<Scalars['DateTime']>;
};
export type PoiInput = {
name: Scalars['String'];
email: Scalars['String'];
lat: Scalars['Float'];
lng: Scalars['Float'];
website?: Maybe<Scalars['String']>;
description?: Maybe<Scalars['String']>;
address: Scalars['String'];
category: Scalars['String'];
relationStatus: Scalars['String'];
image: Scalars['Upload'];
tagIds?: Maybe<Array<Maybe<Scalars['ID']>>>;
};
/** Pagination information about the corresponding list of items. */
export type PageInfo = {
__typename?: 'PageInfo';
/** When paginating forwards, are there more items? */
hasNextPage: Scalars['Boolean'];
/** When paginating backwards, are there more items? */
hasPreviousPage: Scalars['Boolean'];
/** When paginating backwards, the cursor to continue. */
startCursor?: Maybe<Scalars['String']>;
/** When paginating forwards, the cursor to continue. */
endCursor?: Maybe<Scalars['String']>;
/** Total number of node in connection. */
total?: Maybe<Scalars['Int']>;
/** Count of nodes in current request. */
count?: Maybe<Scalars['Int']>;
/** Current page of request. */
currentPage?: Maybe<Scalars['Int']>;
/** Last page in connection. */
lastPage?: Maybe<Scalars['Int']>;
};
/** Pagination information about the corresponding list of items. */
export type PaginatorInfo = {
__typename?: 'PaginatorInfo';
/** Total count of available items in the page. */
count: Scalars['Int'];
/** Current pagination page. */
currentPage: Scalars['Int'];
/** Index of first item in the current page. */
firstItem?: Maybe<Scalars['Int']>;
/** If collection has more pages. */
hasMorePages: Scalars['Boolean'];
/** Index of last item in the current page. */
lastItem?: Maybe<Scalars['Int']>;
/** Last page number of the collection. */
lastPage: Scalars['Int'];
/** Number of items per page in the collection. */
perPage: Scalars['Int'];
/** Total items available in the collection. */
total: Scalars['Int'];
};
export type Query = {
__typename?: 'Query';
pois?: Maybe<Array<Maybe<Poi>>>;
poi?: Maybe<Poi>;
};
export type QueryPoiArgs = {
id?: Maybe<Scalars['ID']>;
};
export type Tag = {
__typename?: 'Tag';
id: Scalars['ID'];
displayName: Scalars['String'];
color: Scalars['String'];
created_at?: Maybe<Scalars['DateTime']>;
updated_at?: Maybe<Scalars['DateTime']>;
};
export type TagInput = {
displayName: Scalars['String'];
color: Scalars['String'];
};
export type User = {
__typename?: 'User';
id: Scalars['ID'];
name: Scalars['String'];
email: Scalars['String'];
created_at: Scalars['DateTime'];
updated_at: Scalars['DateTime'];
};
export type CreatePoiMutationMutationVariables = Exact<{
name: Scalars['String'];
email: Scalars['String'];
lat: Scalars['Float'];
lng: Scalars['Float'];
website?: Maybe<Scalars['String']>;
description?: Maybe<Scalars['String']>;
address: Scalars['String'];
category: Scalars['String'];
relationStatus: Scalars['String'];
image: Scalars['Upload'];
tagIds?: Maybe<Array<Maybe<Scalars['ID']>> | Maybe<Scalars['ID']>>;
}>;
export type CreatePoiMutationMutation = (
{ __typename?: 'Mutation' }
& Pick<Mutation, 'createPoi'>
);
export type CreateTagsMutationMutationVariables = Exact<{
tags: Array<TagInput> | TagInput;
}>;
export type CreateTagsMutationMutation = (
{ __typename?: 'Mutation' }
& { createTags?: Maybe<Array<Maybe<(
{ __typename?: 'Tag' }
& Pick<Tag, 'id'>
)>>> }
);
export const CreatePoiMutationDocument = gql`
mutation createPoiMutation($name: String!, $email: String!, $lat: Float!, $lng: Float!, $website: String, $description: String, $address: String!, $category: String!, $relationStatus: String!, $image: Upload!, $tagIds: [ID]) {
createPoi(
poi: {name: $name, email: $email, lat: $lat, lng: $lng, website: $website, description: $description, address: $address, category: $category, relationStatus: $relationStatus, image: $image, tagIds: $tagIds}
)
}
`;
export const CreateTagsMutationDocument = gql`
mutation createTagsMutation($tags: [TagInput!]!) {
createTags(tags: $tags) {
id
}
}
`;
export type SdkFunctionWrapper = <T>(action: (requestHeaders?:Record<string, string>) => Promise<T>, operationName: string) => Promise<T>;
const defaultWrapper: SdkFunctionWrapper = (action, _operationName) => action();
export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper = defaultWrapper) {
return {
createPoiMutation(variables: CreatePoiMutationMutationVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise<CreatePoiMutationMutation> {
return withWrapper((wrappedRequestHeaders) => client.request<CreatePoiMutationMutation>(CreatePoiMutationDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'createPoiMutation');
},
createTagsMutation(variables: CreateTagsMutationMutationVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise<CreateTagsMutationMutation> {
return withWrapper((wrappedRequestHeaders) => client.request<CreateTagsMutationMutation>(CreateTagsMutationDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'createTagsMutation');
}
};
}
export type Sdk = ReturnType<typeof getSdk>;
\ No newline at end of file
...@@ -10,6 +10,7 @@ export const createPoi = gql` ...@@ -10,6 +10,7 @@ export const createPoi = gql`
$description: String $description: String
$address: String! $address: String!
$category: String! $category: String!
$relationStatus: String!
$image: Upload! $image: Upload!
$tagIds: [ID] $tagIds: [ID]
) { ) {
...@@ -23,6 +24,7 @@ export const createPoi = gql` ...@@ -23,6 +24,7 @@ export const createPoi = gql`
description: $description description: $description
address: $address address: $address
category: $category category: $category
relationStatus: $relationStatus
image: $image image: $image
tagIds: $tagIds tagIds: $tagIds
} }
......
import type { poiData } from './usePoiData';
import { usePoiData } from './usePoiData';
import { useStore } from './useStore';
import type { Tag } from '../types/PointOfInterest';
export interface poiFilteredData extends poiData {
filterTags: Tag[] | null;
setFilterTags: (tags: Tag[]) => void;
filterCategories: string[];
setFilterCategories: (categories: string[]) => void;
}
export const useFilteredPoiData = (): poiFilteredData => {
const { data } = usePoiData();
const filterTags = useStore((store) => store.filterTags);
const setFilterTags = useStore((state) => state.setFilterTags);
const filterCategories = useStore((store) => store.filterCategories);
const setFilterCategories = useStore((state) => state.setFilterCategories);
let filteredData = data;
if (filterTags?.length) {
filteredData = filteredData?.filter(
(poi) => !filterTags.filter((fTag) => !poi.tags.find((tag) => tag.id === fTag.id)).length,
);
}
if (filterCategories?.length) {
filteredData = filteredData?.filter((poi) => !!filterCategories.find((category) => poi.category === category));
}
return {
data: filteredData,
filterTags,
setFilterTags,
filterCategories,
setFilterCategories,
};
};
import type { PointsOfInterestDTO } from 'src/types/PointOfInterest'; import type { PointOfInterest, PointsOfInterestDTO } from 'src/types/PointOfInterest';
import useSWR from 'swr'; import useSWR from 'swr';
export const usePoiData = () => { export interface poiData {
data: PointOfInterest[] | undefined;
}
export const usePoiData = (): poiData => {
const { data } = useSWR<PointsOfInterestDTO>( const { data } = useSWR<PointsOfInterestDTO>(
`{ `{
pois { pois {
...@@ -14,6 +18,7 @@ export const usePoiData = () => { ...@@ -14,6 +18,7 @@ export const usePoiData = () => {
lng lng
image image
category category
relationStatus
tags { tags {
id id
displayName displayName
......
import type { Error } from 'src/types/Error'; import type { Error } from 'src/types/Error';
import type { PointOfInterest } from 'src/types/PointOfInterest'; import type { PointOfInterest, Tag } from 'src/types/PointOfInterest';
import type { Notification } from 'src/types/Notification'; import type { Notification } from 'src/types/Notification';
import type { LatLngTuple } from 'leaflet'; import type { LatLngTuple } from 'leaflet';
import create, { GetState, SetState, State, StateCreator, StoreApi } from 'zustand'; import create, { State } from 'zustand';
import { devtools } from 'zustand/middleware'; import { devtools } from 'zustand/middleware';
interface Store extends State { interface Store extends State {
...@@ -16,6 +16,10 @@ interface Store extends State { ...@@ -16,6 +16,10 @@ interface Store extends State {
setDraftPoi: (latLng: LatLngTuple | null) => void; setDraftPoi: (latLng: LatLngTuple | null) => void;
notification: Notification | null; notification: Notification | null;
setNotification: (notification: Notification | null) => void; setNotification: (notification: Notification | null) => void;
filterTags: Tag[];
setFilterTags: (tags: Tag[]) => void;
filterCategories: string[];
setFilterCategories: (categories: string[]) => void;
} }
// const log = (config: StateCreator<Store>) => (set: SetState<Store>, get: GetState<Store>, api: StoreApi<Store>) => // const log = (config: StateCreator<Store>) => (set: SetState<Store>, get: GetState<Store>, api: StoreApi<Store>) =>
...@@ -59,5 +63,13 @@ export const useStore = create<Store>( ...@@ -59,5 +63,13 @@ export const useStore = create<Store>(
setNotification: (notification) => { setNotification: (notification) => {
set({ notification }); set({ notification });
}, },
filterTags: [],
setFilterTags: (tags) => {
set({ filterTags: tags });
},
filterCategories: [],
setFilterCategories: (categories) => {
set({ filterCategories: categories });
},
})), })),
); );
...@@ -55,7 +55,7 @@ code { ...@@ -55,7 +55,7 @@ code {
} }
} }
.form-input { .form-input-custom {
@apply mt-1 block w-full rounded-lg border-2 border-black border-opacity-20 hover:border-opacity-40 focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50; @apply mt-1 block w-full rounded-lg border-2 border-black border-opacity-20 hover:border-opacity-40 focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50;
} }
......
export interface PointOfInterestBase { export interface PointOfInterestBase {
lat: number; lat: number;
lng: number; lng: number;
name: string; name: string;
description: string; description: string;
address: string; address: string;
website: string; website: string;
category: string; category: string;
} relationStatus: string;
export interface PointOfInterest extends PointOfInterestBase { }
id: number; export interface PointOfInterest extends PointOfInterestBase {
image?: string; id: number;
tags: Tag[]; image?: string;
} tags: Tag[];
}
export interface PointOfInterestFormData extends PointOfInterestBase {
email: string; export interface PointOfInterestFormData extends PointOfInterestBase {
image: File | null; email: string;
tags: number[]; image: File | null;
} tags: number[];
}
export interface Tag {
id: string; export interface Tag {
displayName: string; id: string;
color: string; displayName: string;
} color: string;
}
export interface PointsOfInterestDTO {
pois: PointOfInterest[]; export interface PointsOfInterestDTO {
} pois: PointOfInterest[];
}
export function removeDuplicateObjects<T>(arr: T[], key: keyof T) { export function removeDuplicateObjects<T>(arr: T[], key: keyof T): T[] {
const tagMap = new Map(arr.map((item: T) => [item[key], item])); const tagMap = new Map(arr.map((item: T) => [item[key], item]));
return [...tagMap.values()]; return [...tagMap.values()];
} }
export function generateRandomHslColor(saturation = 100, lightness = 50) { export function generateRandomHslColor(saturation = 100, lightness = 50, distinctValues = 20): string {
const randomHue = getRandomIntInclusive(0, 360); const randomColorIndex = getRandomIntInclusive(0, distinctValues);
const color = `hsl(${randomHue},${saturation}%,${lightness}%)`; const color = `hsl(${Math.floor(randomColorIndex * (360 / distinctValues))},${saturation}%,${lightness}%)`;
return color; return color;
} }
......
import { request } from 'graphql-request'; import { request } from 'graphql-request';
import type { Variables } from 'graphql-request/dist/types';
const fetcher = (query: string, variables?: Object) => const fetcher = <T, V extends Variables>(query: string, variables?: V): Promise<T> =>
request(import.meta.env.SNOWPACK_PUBLIC_API_URL, query, variables); request(import.meta.env.SNOWPACK_PUBLIC_API_URL, query, variables);
export default fetcher; export default fetcher;