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 committed Dec 18, 2024
1 parent cb6980a commit d40b319
Show file tree
Hide file tree
Showing 7 changed files with 367 additions and 20 deletions.
3 changes: 3 additions & 0 deletions changelog/_8653.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
### Added

- Added `SearchProfiles` component for kiezradar search profile list view
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import json

from django import template
from django.urls import reverse
from django.utils.html import format_html

register = template.Library()


@register.simple_tag()
def react_kiezradar_search_profiles():
attributes = {}
attributes = {
"apiUrl": reverse("searchprofiles-list"),
"planListUrl": reverse("meinberlin_plans:plan-list"),
}

return format_html(
'<div data-mb-widget="kiezradar-search-profiles" '
Expand Down
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;
}
1 change: 1 addition & 0 deletions meinberlin/assets/scss/style_user_facing.scss
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
@import "components_user_facing/phase_info";
@import "components_user_facing/pill";
@import "components_user_facing/rating";
@import "components_user_facing/search-profiles";
@import "components_user_facing/searchform-slot";
@import "components_user_facing/service-panel";
@import "components_user_facing/tabnavigation";
Expand Down
252 changes: 252 additions & 0 deletions meinberlin/react/kiezradar/SearchProfiles.jsx
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>
)
}
7 changes: 0 additions & 7 deletions meinberlin/react/kiezradar/dummy.jsx

This file was deleted.

Loading

0 comments on commit d40b319

Please sign in to comment.