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

Feature/tree filter to map #154

Merged
merged 23 commits into from
Oct 14, 2024
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 20 additions & 12 deletions frontend/src/components/general/buttons/FilterButton.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,32 @@
import React from 'react';
import React from 'react'

interface FilterButtonProps {
ariaLabel: string;
activeCount: number;
onClick: () => void;
ariaLabel: string
activeCount: number
isOnMap: boolean
onClick: () => void
}

const FilterButton: React.FC<FilterButtonProps> = ({ ariaLabel, activeCount, onClick }) => {

const FilterButton: React.FC<FilterButtonProps> = ({
ariaLabel,
activeCount,
onClick,
isOnMap,
}) => {
return (
<button
<button
aria-label={ariaLabel}
id="filter-button"
aria-selected={activeCount > 0}
className={`border border-green-light px-5 py-2 font-medium rounded-full flex items-center gap-x-2 transition-colors ease-in-out duration-300 ${activeCount > 0 ? 'bg-green-light-200' : ''} hover:bg-green-light-200 hover:border-transparent`}
className={`relative font-nunito-sans text-base ${isOnMap ? 'z-[1000] shadow-cards' : ''} bg-white border border-green-light px-5 py-2 font-medium rounded-full flex items-center gap-x-2 transition-colors ease-in-out duration-300 ${activeCount > 0 ? 'bg-green-light-200' : ''} hover:bg-green-light-200`}
onClick={onClick}
>
Filter
<span className="block bg-green-dark/20 w-6 h-6 rounded-full">{activeCount}</span>
<span className="block bg-green-dark/20 w-6 h-6 rounded-full">
{activeCount}
</span>
</button>
);
};
)
}

export default FilterButton;
export default FilterButton
39 changes: 21 additions & 18 deletions frontend/src/components/general/cards/DashboardCard.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,41 @@
import { Link } from '@tanstack/react-router';
import { MoveRight } from 'lucide-react';
import React from 'react';
import { Link } from '@tanstack/react-router'
import { MoveRight } from 'lucide-react'
import React from 'react'

interface DashboardCard {
headline: string;
description: string;
linkLabel: string;
url: string;
theme: "dark" | "light" | "white";
headline: string
description: string
linkLabel: string
url: string
theme: 'dark' | 'light' | 'white'
}

const DashboardCard: React.FC<DashboardCard> = ({ headline, description, linkLabel, url, theme }) => {
const DashboardCard: React.FC<DashboardCard> = ({
headline,
description,
linkLabel,
url,
theme,
}) => {
const themeClasses = {
dark: 'border-green-dark bg-green-dark-50 hover:bg-green-dark-100',
light: 'border-green-light bg-green-light-50 hover:bg-green-light-100',
white: 'border-dark-50 bg-white hover:bg-dark-100'
};
white: 'border-dark-50 bg-white hover:bg-dark-100',
}
return (

<Link
to={url}
aria-label={linkLabel}
className={`shadow-cards border h-full p-6 rounded-xl group flex flex-col gap-4 transition-all ease-in-out duration-300 ${themeClasses[theme]}`}>
className={`shadow-cards border h-full p-6 rounded-xl group flex flex-col gap-4 transition-all ease-in-out duration-300 ${themeClasses[theme]}`}
>
<h3 className="font-lato text-lg text-dark font-semibold">{headline}</h3>
<p>{description}</p>
<p className="font-lato font-semibold text-green-dark flex items-center gap-x-2">
<span>{linkLabel}</span>
<MoveRight className="transition-all ease-in-out duration-300 group-hover:translate-x-2" />
</p>
</Link>
);
)
}




export default DashboardCard;
export default DashboardCard
266 changes: 134 additions & 132 deletions frontend/src/components/general/filter/Dialog.tsx
Original file line number Diff line number Diff line change
@@ -1,146 +1,148 @@
import useTreeclusterFilter from '@/hooks/useTreeclusterFilter'
import React, { useState } from 'react';
import FilterButton from '../buttons/FilterButton';
import PrimaryButton from '../buttons/PrimaryButton';
import SecondaryButton from '../buttons/SecondaryButton';
import { X } from 'lucide-react';
import useOutsideClick from '@/hooks/useOutsideClick';
import Option from './Option';
import { useNavigate } from '@tanstack/react-router';
import { useSuspenseQuery } from '@tanstack/react-query';
import { EntitiesWateringStatus } from '@/api/backendApi';
import { getWateringStatusDetails } from '@/hooks/useDetailsForWateringStatus';
import { regionsQuery } from '@/api/queries';
import {
ForwardedRef,
forwardRef,
useEffect,
useImperativeHandle,
useState,
} from 'react'
import FilterButton from '../buttons/FilterButton'
import PrimaryButton from '../buttons/PrimaryButton'
import SecondaryButton from '../buttons/SecondaryButton'
import { X } from 'lucide-react'
import useOutsideClick from '@/hooks/useOutsideClick'
import { useNavigate } from '@tanstack/react-router'
import useFilter from '@/hooks/useFilter'
import { Filters } from '@/context/FilterContext'

interface DialogProps {
initStatusTags: string[]
initRegionTags: string[]
headline: string
fullUrlPath: string
applyFilter: (statusTags: string[], regionTags: string[]) => void
children?: React.ReactNode
isOnMap?: boolean
onApplyFilters: () => void
onResetFilters: () => void
}

const Dialog: React.FC<DialogProps> = ({ initStatusTags, initRegionTags, headline, fullUrlPath, applyFilter }) => {
const [isOpen, setIsOpen] = useState(false);
const navigate = useNavigate({ from: fullUrlPath })
const dialogRef = useOutsideClick(() => close())
const { data: regionRes } = useSuspenseQuery(regionsQuery());

const {
filters,
setFilters,
appliedFilters,
handleFilterChange,
resetFilters,
applyFilters,
} = useTreeclusterFilter(initStatusTags, initRegionTags)

const resetAndClose = () => {
resetFilters(applyFilter)
setIsOpen(false)
navigate({ search: () => ({}) })
}
const Dialog = forwardRef(
(
{
headline,
fullUrlPath,
onApplyFilters,
onResetFilters,
children,
isOnMap = false,
}: DialogProps,
ref: ForwardedRef<HTMLDivElement>
) => {
const [isOpen, setIsOpen] = useState(false)
const [oldValues, setOldValues] = useState<Filters>({
statusTags: [],
regionTags: [],
hasCluster: undefined,
plantingYears: [],
})
const [count, setCount] = useState(0);
const navigate = useNavigate({ from: fullUrlPath })
useImperativeHandle(ref, () => dialogRef.current)

const close = () => {
setIsOpen(false)
setFilters({
statusTags: appliedFilters.statusTags,
regionTags: appliedFilters.regionTags,
const dialogRef = useOutsideClick(() => {
if (!isOpen) return
handleClose()
})
}

const handleApplyFilters = () => {
applyFilters(applyFilter)
setIsOpen(false)
const { filters, resetFilters, applyOldStateToTags } = useFilter()

navigate({
search: () => ({
status: filters.statusTags.length > 0 ? filters.statusTags : undefined,
region: filters.regionTags.length > 0 ? filters.regionTags : undefined,
}),
})
}
const handleSubmit = () => {
onApplyFilters()
setIsOpen(false)
setIsOpen(false)

navigate({
search: () => ({
status:
filters.statusTags.length > 0 ? filters.statusTags : undefined,
region:
filters.regionTags.length > 0 ? filters.regionTags : undefined,
hasCluster:
filters.hasCluster ?? undefined,
plantingYears:
filters.plantingYears.length > 0 ? filters.plantingYears : undefined
}),
})
}

const handleReset = () => {
onResetFilters()
applyOldStateToTags({ statusTags: [], regionTags: [], hasCluster: undefined, plantingYears: [] })
resetFilters()
setIsOpen(false)
navigate({ search: () => ({}) })
doriengr marked this conversation as resolved.
Show resolved Hide resolved
}

const handleClose = () => {
setIsOpen(false)
applyOldStateToTags(oldValues)
}

return (
<div>
<div
className={`bg-dark-900/90 fixed inset-0 z-50 ${isOpen ? 'block' : 'hidden'}`}
></div>

<FilterButton
activeCount={
appliedFilters.statusTags.length + appliedFilters.regionTags.length
}
ariaLabel={headline}
onClick={() => {
isOpen ? setIsOpen(false) : setIsOpen(true)
}}
/>

<section
ref={dialogRef}
role="dialog"
aria-modal="true"
className={`fixed z-[60] inset-x-4 shadow-xl bg-white top-1/2 -translate-y-1/2 p-5 rounded-xl mx-auto max-w-[30rem] ${isOpen ? 'block' : 'hidden'}`}
>
<div className="flex items-center justify-between gap-x-5 mb-5">
<h2 className="text-xl font-lato font-semibold">{headline}</h2>
<button
aria-label="Close Dialog"
className="text-dark-400 hover:text-dark-600 stroke-1"
onClick={close}
>
<X />
</button>
</div>

<fieldset>
<legend className="font-lato font-semibold text-dark-600 mb-2">
Zustand der Bewässerung:
</legend>
{Object.entries(EntitiesWateringStatus).map(
([statusKey, statusValue]) => (
<Option
key={statusKey}
label={getWateringStatusDetails(statusValue).label}
name={statusKey}
checked={filters.statusTags.includes(
getWateringStatusDetails(statusValue).label
)}
onChange={handleFilterChange('status')}
>
<div
className={`bg-${getWateringStatusDetails(statusValue).color} w-4 h-4 rounded-full`}
/>
</Option>
)
)}
</fieldset>

<fieldset className="mt-6">
<legend className="font-lato font-semibold text-dark-600 mb-2">Stadtteil in Flensburg:</legend>
{regionRes.regions.map((region) => (
<Option
key={region.id}
label={region.name}
name={String(region.id)}
checked={filters.regionTags.includes(region.name)}
onChange={handleFilterChange('region')}
const openFilter = () => {
setOldValues(filters)
setIsOpen(true)
}

useEffect(() => {
setCount(filters.statusTags.length
+ filters.regionTags.length
+ (filters.hasCluster !== undefined ? 1 : 0)
+ filters.plantingYears.length);
}, [filters])

return (
<div className="font-nunito-sans text-base">
<div
className={`bg-dark-900/90 fixed inset-0 z-[1020] ${isOpen ? 'block' : 'hidden'}`}
></div>

<FilterButton
activeCount={count}
ariaLabel={headline}
isOnMap={isOnMap}
onClick={() => {
isOpen ? setIsOpen(false) : openFilter()
}}
/>

<section
ref={dialogRef}
role="dialog"
aria-modal="true"
className={`fixed max-h-[80dvh] overflow-y-auto z-[1030] inset-x-4 shadow-xl bg-white top-1/2 -translate-y-1/2 p-5 rounded-xl mx-auto max-w-[30rem] ${isOpen ? 'block' : 'hidden'}`}
>
<div className="flex items-center justify-between gap-x-5 mb-5">
<h2 className="text-xl font-lato font-semibold">{headline}</h2>
<button
aria-label="Close Dialog"
className="text-dark-400 hover:text-dark-600 stroke-1"
onClick={handleClose}
>
<X />
</button>
</div>

{children}

<div className="flex flex-wrap gap-5 mt-6">
<PrimaryButton
label="Anwenden"
type="button"
onClick={handleSubmit}
/>
))}
</fieldset>

<div className="flex flex-wrap gap-5 mt-6">
<PrimaryButton
label="Anwenden"
type="button"
onClick={handleApplyFilters}
/>
<SecondaryButton label="Zurücksetzen" onClick={resetAndClose} />
</div>
</section>
</div>
)
}
<SecondaryButton label="Zurücksetzen" onClick={handleReset} />
</div>
</section>
</div>
)
}
)

export default Dialog
Loading