-
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
cb6980a
commit d40b319
Showing
7 changed files
with
367 additions
and
20 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
### Added | ||
|
||
- Added `SearchProfiles` component for kiezradar search profile list view |
6 changes: 5 additions & 1 deletion
6
meinberlin/apps/kiezradar/templatetags/react_kiezradar_search_profiles.py
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
96 changes: 96 additions & 0 deletions
96
meinberlin/assets/scss/components_user_facing/_search-profiles.scss
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,96 @@ | ||
.search-profiles-list__title { | ||
margin-bottom: 1rem; | ||
|
||
@media screen and (min-width: $breakpoint-palm) { | ||
margin-bottom: 1.5rem; | ||
} | ||
} | ||
|
||
.search-profile { | ||
border-bottom: 1px solid $gray-lighter; | ||
padding-bottom: 1rem; | ||
padding-top: 1rem; | ||
|
||
@media screen and (min-width: $breakpoint-palm) { | ||
padding-bottom: 1.5rem; | ||
padding-top: 1.5rem; | ||
} | ||
} | ||
|
||
.search-profile__title { | ||
margin-bottom: 0; | ||
margin-top: 0; | ||
} | ||
|
||
.search-profile__header { | ||
display: flex; | ||
gap: 3rem; | ||
margin-bottom: 1.2rem; | ||
} | ||
|
||
.search-profile__header-buttons { | ||
display: none; | ||
|
||
@media screen and (min-width: $breakpoint-palm) { | ||
display: block; | ||
margin-left: auto; | ||
} | ||
} | ||
|
||
.search-profile__buttons { | ||
display: flex; | ||
gap: 1.5rem; | ||
} | ||
|
||
.search-profile__button { | ||
padding: 0; | ||
margin: 0; | ||
font-size: $em-spacer; | ||
line-height: 1.2; | ||
display: inline-block; | ||
text-align: center; | ||
cursor: pointer; | ||
color: var(--color-black); | ||
} | ||
|
||
.search-profile__button:hover, | ||
.search-profile__button:focus { | ||
text-decoration: underline; | ||
} | ||
|
||
.search-profiles-list__error, | ||
.search-profile__error { | ||
color: $danger; | ||
} | ||
|
||
.search-profile__form { | ||
margin-bottom: 1.2rem; | ||
} | ||
|
||
.search-profile__footer { | ||
@media screen and (min-width: $breakpoint-palm) { | ||
display: flex; | ||
} | ||
} | ||
|
||
.search-profile__view-projects { | ||
display: block; | ||
|
||
@media screen and (min-width: $breakpoint-palm) { | ||
display: inline; | ||
margin-left: auto; | ||
} | ||
} | ||
|
||
.search-profile__footer-buttons { | ||
display: flex; | ||
margin-top: 1rem; | ||
|
||
@media screen and (min-width: $breakpoint-palm) { | ||
display: none; | ||
} | ||
} | ||
|
||
.search-profile__footer-buttons div:only-of-type { | ||
margin-left: auto; | ||
} |
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,252 @@ | ||
import cookie from 'js-cookie' | ||
import React, { useState, useEffect } from 'react' | ||
import django from 'django' | ||
import Spinner from '../contrib/Spinner' | ||
|
||
const titleText = django.gettext('Search Profiles') | ||
const descriptionText = django.gettext( | ||
'In this area you manage your search profiles.' | ||
) | ||
const noSavedProfilesText = django.gettext('No saved search profiles') | ||
const findProjectsText = django.gettext('Find projects') | ||
const yourSavedProfilesText = django.gettext('Your saved search profiles') | ||
const renameText = django.gettext('Rename') | ||
const deleteText = django.gettext('Delete') | ||
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 errorSearchProfilesText = django.gettext( | ||
'Failed to fetch search profiles' | ||
) | ||
const errorDeleteSearchProfilesText = django.gettext( | ||
'Failed to delete search profile' | ||
) | ||
const errorUpdateSearchProfilesText = django.gettext( | ||
'Failed to update search profile' | ||
) | ||
|
||
export default function SearchProfiles (props) { | ||
return ( | ||
<> | ||
<h1>{titleText}</h1> | ||
<p>{descriptionText}</p> | ||
<SearchProfileList {...props} /> | ||
</> | ||
) | ||
} | ||
|
||
function SearchProfileList ({ apiUrl, planListUrl }) { | ||
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 ( | ||
<div> | ||
{searchProfiles.length === 0 | ||
? ( | ||
<> | ||
<h2 className="search-profiles-list__title">{noSavedProfilesText}</h2> | ||
<a href={planListUrl} className="button button--light"> | ||
<i className="fa-solid fa-magnifying-glass mr-1" /> | ||
{findProjectsText} | ||
</a> | ||
</> | ||
) | ||
: ( | ||
<> | ||
<h2 className="search-profiles-list__title"> | ||
{yourSavedProfilesText} ({searchProfiles.length}) | ||
</h2> | ||
{searchProfiles.map((profile) => ( | ||
<SearchProfile | ||
key={profile.id} | ||
apiUrl={apiUrl} | ||
planListUrl={planListUrl} | ||
profile={profile} | ||
onDelete={(id) => | ||
setSearchProfiles((prevSearchProfiles) => | ||
prevSearchProfiles.filter((profile) => profile.id !== id) | ||
)} | ||
/> | ||
))} | ||
</> | ||
)} | ||
</div> | ||
) | ||
} | ||
|
||
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 fetch(apiUrl + profile.id + '/', { | ||
headers: { | ||
'X-CSRFToken': cookie.get('csrftoken') | ||
}, | ||
method: '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 fetch(apiUrl + profile.id + '/', { | ||
headers: { | ||
'Content-Type': 'application/json; charset=utf-8', | ||
'X-CSRFToken': cookie.get('csrftoken') | ||
}, | ||
method: 'PATCH', | ||
body: JSON.stringify({ name: e.target.elements.name.value }) | ||
}) | ||
|
||
if (!response.ok) { | ||
throw new Error(errorUpdateSearchProfilesText) | ||
} | ||
|
||
const data = await response.json() | ||
setProfile(data) | ||
} catch (err) { | ||
setError(err.message) | ||
} finally { | ||
setIsEditing(false) | ||
setLoading(false) | ||
} | ||
} | ||
|
||
return ( | ||
<div className="search-profile"> | ||
<div className="search-profile__header"> | ||
<h3 className="search-profile__title">{profile.name}</h3> | ||
{!isEditing && ( | ||
<div className="search-profile__header-buttons"> | ||
<Buttons | ||
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"> | ||
<Buttons | ||
onEdit={() => setIsEditing(true)} | ||
onDelete={handleDelete} | ||
loading={loading} | ||
/> | ||
</div> | ||
)} | ||
</div> | ||
</div> | ||
) | ||
} | ||
|
||
function Buttons ({ 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 was deleted.
Oops, something went wrong.
Oops, something went wrong.