-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
react/kiez-search-profile: add initial dashboard
- Loading branch information
1 parent
ee49b2b
commit 1b5e0d6
Showing
6 changed files
with
310 additions
and
298 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
) | ||
}} | ||
/> | ||
)) | ||
)} | ||
</> | ||
) | ||
} |
Oops, something went wrong.