Skip to content

Commit

Permalink
react/kiez-search-profile: add initial dashboard
Browse files Browse the repository at this point in the history
  • Loading branch information
sevfurneaux authored and vellip committed Jan 8, 2025
1 parent ee49b2b commit 1b5e0d6
Show file tree
Hide file tree
Showing 6 changed files with 310 additions and 298 deletions.
42 changes: 23 additions & 19 deletions meinberlin/assets/scss/components_user_facing/_search-profiles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,28 @@
}

.search-profile__title {
margin-bottom: 0;
margin-bottom: 0.25rem;
margin-top: 0;
}

.search-profile__title:last-child {
margin-bottom: 0;
}

.search-profile__filters {
color: var(--color-grey-darkest);
font-size: 0.9rem;
margin-bottom: 0;
padding-left: 0;
}

.search-profile__filter {
display: inline-block;
list-style: none;
margin-right: 1rem;
margin-bottom: 0;
}

.search-profile__header {
display: flex;
gap: 3rem;
Expand Down Expand Up @@ -50,11 +68,10 @@
display: inline-block;
text-align: center;
cursor: pointer;
color: var(--color-black);
color: $text-base;
}

.search-profile__button:hover,
.search-profile__button:focus {
.search-profile__button:hover {
text-decoration: underline;
}

Expand Down Expand Up @@ -96,22 +113,9 @@
}

.search-profile__alert {
left: 0;
right: 0;
pointer-events: none;
position: fixed;
padding-left: 0.75rem;
padding-right: 0.75rem;
top: 0;
width: 100%;
z-index: 50;
}

.search-profile__alert-container {
margin: 0 auto;
pointer-events: auto;
margin-top: -1.5em;

@media screen and (min-width: $breakpoint-tablet-landscape) {
width: clamp(980px, 50vw, 100%);
margin-top: -2.5em;
}
}
137 changes: 137 additions & 0 deletions meinberlin/react/kiezradar/SearchProfile.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import React, { useState } from 'react'
import django from 'django'
import SearchProfileButtons from './SearchProfileButtons'
import { updateItem } from '../contrib/helpers'

const renameSearchProfileText = django.gettext('Rename search profile')
const cancelText = django.gettext('Cancel')
const saveText = django.gettext('Save')
const savingText = django.gettext('Saving')
const viewProjectsText = django.gettext('View projects')
const errorText = django.gettext('Error')
const errorDeleteSearchProfilesText = django.gettext(
'Failed to delete search profile'
)
const errorUpdateSearchProfilesText = django.gettext(
'Failed to update search profile'
)

export default function SearchProfile ({ apiUrl, planListUrl, profile: profile_, onDelete }) {
const [isEditing, setIsEditing] = useState(false)
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
const [profile, setProfile] = useState(profile_)

const handleDelete = async () => {
setLoading(true)
setError(null)

try {
const response = await updateItem({}, apiUrl + profile.id + '/', 'DELETE')

if (!response.ok) {
throw new Error(errorDeleteSearchProfilesText)
}

onDelete(profile.id)
} catch (err) {
setError(err.message)
} finally {
setLoading(false)
}
}

const handleSubmit = async (e) => {
e.preventDefault()
setLoading(true)
setError(null)

try {
const response = await updateItem({ name: e.target.elements.name.value }, apiUrl + profile.id + '/', 'PATCH')

if (!response.ok) {
throw new Error(errorUpdateSearchProfilesText)
}

const data = await response.json()
setProfile(data)
} catch (err) {
setError(err.message)
} finally {
setIsEditing(false)
setLoading(false)
}
}

const filters = [
profile.districts,
profile.project_types,
profile.topics,
profile.organisations
]
.map((filter) => filter.map(({ name }) => name))
.map((names) => names.join(', '))

return (
<div className="search-profile">
<div className="search-profile__header">
<div>
<h3 className="search-profile__title">{profile.name}</h3>
<ul className="search-profile__filters">
{filters.map((filter) => (
<li key={filter} className="search-profile__filter">{filter}</li>
))}
</ul>
</div>
{!isEditing && (
<div className="search-profile__header-buttons">
<SearchProfileButtons
onEdit={() => setIsEditing(true)}
onDelete={handleDelete}
loading={loading}
/>
</div>
)}
</div>
{error && <div className="search-profile__error">{errorText + ': ' + error}</div>}
{isEditing && (
<form className="form--base panel--heavy search-profile__form" onSubmit={handleSubmit}>
<div className="form-group">
<label htmlFor="name">{renameSearchProfileText}</label>
<input id="name" name="name" type="text" required />
</div>
<div className="form-actions">
<div className="form-actions__left">
<button className="link" onClick={() => setIsEditing(false)}>
{cancelText}
</button>
</div>
<div className="form-actions__right">
<button
className="button"
type="submit"
disabled={loading}
>
{loading ? savingText + '...' : saveText}
</button>
</div>
</div>
</form>
)}
<div className="search-profile__footer">
<a href={planListUrl + '?search-profile=' + profile.id} className="button button--light search-profile__view-projects">
{viewProjectsText}
</a>
{!isEditing && (
<div className="search-profile__footer-buttons">
<SearchProfileButtons
onEdit={() => setIsEditing(true)}
onDelete={handleDelete}
loading={loading}
/>
</div>
)}
</div>
</div>
)
}
37 changes: 37 additions & 0 deletions meinberlin/react/kiezradar/SearchProfileAlert.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React from 'react'
import django from 'django'

const alertHeadlineText = django.gettext(
'Search profile successfully deleted'
)
const alertText = django.gettext('Your changes have been deleted.')

export default function SearchProfileAlert ({ onClose }) {
return (
<div className="search-profile__alert">
<div
className="alert alert--success"
role="alert"
aria-live="polite"
aria-atomic="true"
>
<div className="alert__content">
<h3 className="alert__headline">
{alertHeadlineText}
</h3>
<p className="alert__text">
{alertText}
</p>
<button
type="button"
className="alert__close"
onClick={onClose}
aria-label="Close"
>
<i className="fa fa-times" aria-hidden="true" />
</button>
</div>
</div>
</div>
)
}
24 changes: 24 additions & 0 deletions meinberlin/react/kiezradar/SearchProfileButtons.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from 'react'
import django from 'django'

const renameText = django.gettext('Rename')
const deleteText = django.gettext('Delete')

export default function SearchProfileButtons ({ onEdit, onDelete, loading }) {
return (
<div className="search-profile__buttons">
<button className="search-profile__button" onClick={onEdit}>
<i className="fa-solid fa-pencil mr-1" />
{renameText}
</button>
<button
className="search-profile__button"
onClick={onDelete}
disabled={loading}
>
<i className="fa-classic fa-regular fa-trash-can mr-1" />
{deleteText}
</button>
</div>
)
}
82 changes: 82 additions & 0 deletions meinberlin/react/kiezradar/SearchProfileList.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import React, { useState, useEffect } from 'react'
import django from 'django'
import Spinner from '../contrib/Spinner'
import SearchProfile from './SearchProfile'

const noSavedProfilesText = django.gettext('No saved search profiles')
const findProjectsText = django.gettext('Find projects')
const yourSavedProfilesText = django.gettext('Your saved search profiles')
const errorText = django.gettext('Error')
const errorSearchProfilesText = django.gettext(
'Failed to fetch search profiles'
)

export default function SearchProfileList ({ apiUrl, planListUrl, onAlert }) {
const [searchProfiles, setSearchProfiles] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)

useEffect(() => {
const fetchSearchProfiles = async () => {
try {
setLoading(true)
setError(null)

const response = await fetch(apiUrl)

if (!response.ok) {
throw new Error(errorSearchProfilesText)
}

const data = await response.json()
setSearchProfiles(data)
} catch (err) {
setError(err.message)
} finally {
setLoading(false)
}
}
fetchSearchProfiles()
}, [])

if (loading) {
return <Spinner />
}

if (error) {
return (
<div className="search-profiles-list__error">
{errorText}: {error}
</div>
)
}

return (
<>
<h2>{searchProfiles.length === 0 ? noSavedProfilesText : yourSavedProfilesText + ' ' + searchProfiles.length}</h2>
{searchProfiles.length === 0
? (
<a href={planListUrl} className="button button--light">
<i className="fa-solid fa-magnifying-glass mr-1" />
{findProjectsText}
</a>
)
: (
searchProfiles.map((profile) => (
<SearchProfile
key={profile.id}
apiUrl={apiUrl}
planListUrl={planListUrl}
profile={profile}
onDelete={(id) => {
onAlert()
setSearchProfiles((prevSearchProfiles) =>
prevSearchProfiles.filter((profile) => profile.id !== id)
)
}}
/>
))
)}
</>
)
}
Loading

0 comments on commit 1b5e0d6

Please sign in to comment.