Skip to content

Commit

Permalink
Edit announcement and contact card (#199)
Browse files Browse the repository at this point in the history
* Adding google maps package

* Set secret path for docker swar

* Debugging secrets

* Debugging secrets

* Debugging secrets

* Backend secret backend setted up

* Backend secret backend setted up v2

* Backend secret backend setted up v3

* Backend secret backend setted up v4

* Switch to UV for dependency and venv management

* Remove GitHub uv cache

* Missing dependencies

* powershell -> pwsh

* group tools vs stubs in dev dependencies

* sync djangorestframework-stubs higher

* Fix standalone isntaller to actually use Unix command

* try adding bin to path

* Add ref to astral-sh/uv#5964

* Remove dependency on pytz

* Edit Contact works fine

* Adding links edition , TODO add error validation

* Announcement edition OK , still TODO: error handling

* Fixing some html keys duplication

* Cardinal edit site fixed

* WIP

* Fix adress

* Error handling OK, TODO : Translation

* Translations OK

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Fixing lint and few build errors

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Fixing mypy and pyrights warns

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Line to long for ruff ><'

* Possibility to generate spec file by cli

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Rollbacking some mistakes

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Ruff don't like bare exception, it is not really needed

* Build test

* Regenerating package.lock

* Revert "Possibility to generate spec file by cli"

This reverts commit 000da46.

* Update package-lock

* Update canopeum_frontend/src/components/analytics/site-modal/SiteModal.tsx

Co-authored-by: Samuel T. <samuel.therrien@beslogic.com>

* Update canopeum_frontend/src/components/analytics/site-modal/SiteModal.tsx

Co-authored-by: Samuel T. <samuel.therrien@beslogic.com>

* Update canopeum_backend/canopeum_backend/views.py

Co-authored-by: Samuel T. <samuel.therrien@beslogic.com>

* Rollback config database changes, opening another PR for these

* Removing indexing list because unecessary since dev test previously created same treetype on a single batch but shouldn't

* Simplifying things

* Lint autofix

* Mass linting

* Reason mandatory onCancel SiteModal + Bringing back StrictMode

* Update canopeum_frontend/src/components/social/AnnouncementCard.tsx

Co-authored-by: Samuel T. <samuel.therrien@beslogic.com>

* Update canopeum_frontend/src/components/social/AnnouncementCard.tsx

Co-authored-by: Samuel T. <samuel.therrien@beslogic.com>

* Rolling back to fit dprint requirements + deletion of requirement.txt

---------

Co-authored-by: Theo Auffeuvre <theo.auffeuvre@beslogic.com>
Co-authored-by: Samuel Therrien <samuel.therrien@beslogic.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
  • Loading branch information
4 people committed Aug 26, 2024
1 parent 5eb5bd5 commit 2647b45
Show file tree
Hide file tree
Showing 30 changed files with 672 additions and 151 deletions.
14 changes: 9 additions & 5 deletions canopeum_backend/canopeum_backend/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,11 +192,15 @@ def from_dms_lat_long(cls, dms_latitude: str, dms_longitude: str):
if dms_longitude_split[3] == "W":
dd_longitude *= -1

formatted_address = (
gmaps.reverse_geocode((dd_latitude, dd_longitude), result_type="street_address")[0] # pyright: ignore[reportAttributeAccessIssue] -- No type stub currently exists
if gmaps is not None
else ""
)
if gmaps is not None:
data_retrieved = gmaps.reverse_geocode( # pyright: ignore[reportAttributeAccessIssue] -- No type stub currently exists
(dd_latitude, dd_longitude), result_type="street_address"
)
formatted_address = (
data_retrieved[0]["formatted_address"] if data_retrieved else "Custom address"
)
else:
formatted_address = "Unknown address"

return cls.objects.create(
dms_latitude=dms_latitude,
Expand Down
2 changes: 1 addition & 1 deletion canopeum_backend/canopeum_backend/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
),
# Contact
path(
"social/sites/<int:siteId>/contacts/<int:contactId>/",
"social/contacts/<int:contactId>/",
views.ContactDetailAPIView.as_view(),
name="contact-detail",
),
Expand Down
10 changes: 4 additions & 6 deletions canopeum_backend/canopeum_backend/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -728,14 +728,12 @@ class AnnouncementDetailAPIView(APIView):
operation_id="announcement_update",
)
def patch(self, request: Request, siteId):
try:
announcement = Announcement.objects.get(site=siteId)
except Announcement.DoesNotExist:
return Response(status=status.HTTP_404_NOT_FOUND)
announcement = Announcement.objects.get_or_create(site=siteId)[0]

serializer = AnnouncementSerializer(announcement, data=request.data)
if serializer.is_valid():
serializer.save()
Site.objects.filter(pk=siteId).update(announcement=announcement)
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

Expand All @@ -744,9 +742,9 @@ class ContactDetailAPIView(APIView):
@extend_schema(
request=ContactSerializer, responses=ContactSerializer, operation_id="contact_update"
)
def patch(self, request: Request, pk):
def patch(self, request: Request, contactId):
try:
contact = Contact.objects.get(pk=pk)
contact = Contact.objects.get(pk=contactId)
except Contact.DoesNotExist:
return Response(status=status.HTTP_404_NOT_FOUND)

Expand Down
32 changes: 0 additions & 32 deletions canopeum_frontend/src/components/AnnouncementCard.tsx

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ const OptionQuantitySelector = <TValue extends OptionQuantityValueType>(
setSelectedOptions(updated)
}

useEffect((): void => setSelectedOptions(selected), [selected])

const removeType = (option: SelectorOption<TValue>) => {
setSelectedOptions(
selectedOptions.filter(optionQuantity => optionQuantity.option.value !== option.value),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,17 @@ const TreeSpeciesSelector = (
const [options, setOptions] = useState<SelectorOption<number>[]>([])
const [selected, setSelected] = useState<SelectorOptionQuantity<number>[]>([])

const fetchTreeSpecies = useCallback(
async () => {
useEffect(() => {
const fetchTreeSpecies = async () => {
const speciesResponse = await getApiClient().treeClient.species()
setAvailableSpecies(speciesResponse)
setOptions(speciesResponse.map(treeType => ({
value: treeType.id,
displayText: translateValue(treeType),
})))
},
[getApiClient, translateValue],
)

useEffect(() => void fetchTreeSpecies(), [fetchTreeSpecies])
}
void fetchTreeSpecies()
}, [getApiClient, translateValue])

useEffect(() => {
if (!species) return
Expand All @@ -50,7 +48,8 @@ const TreeSpeciesSelector = (
},
quantity: specie.quantity,
})))
}, [species, translateValue])
// eslint-disable-next-line react-hooks/exhaustive-deps -- translateValue is a dependency
}, [availableSpecies, species])

const handleChange = useCallback((selectedOptions: SelectorOptionQuantity<number>[]) => {
const selectedSpecies = selectedOptions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,7 @@ const CreateBatchModal = ({ open, site, handleClose }: Props) => {
species,
supportedSpecieIds,
)
} catch (error: unknown) {
console.error(error)
} catch {
openAlertSnackbar(
t('analyticsSite.batch-modal.feedback.create-error'),
{ severity: 'error' },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,7 @@ const BatchModal = ({ batchToEdit, handleClose }: Props) => {
species,
supportedSpecieIds,
)
} catch (error: unknown) {
console.error(error)
} catch {
openAlertSnackbar(
t('analyticsSite.batch-modal.feedback.edit-error'),
{ severity: 'error' },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ const SiteCoordinates = ({ latitude, longitude, onChange }: Props) => {
const [lat, setLat] = useState<Coordinate>(latitude ?? defaultLatitude)
const [long, setLong] = useState<Coordinate>(longitude ?? defaultLongitude)

useEffect(() => onChange(lat, long), [lat, long, onChange])
// eslint-disable-next-line react-hooks/exhaustive-deps -- onChange is a dependency
useEffect(() => onChange(lat, long), [lat, long])
useEffect(() => latitude && setLat(latitude), [latitude])
useEffect(() => longitude && setLong(longitude), [longitude])

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { getApiBaseUrl } from '@services/apiSettings'
type Props = {
readonly open: boolean,
readonly handleClose: (
reason?: 'backdropClick' | 'escapeKeyDown' | 'save',
reason: 'backdropClick' | 'escapeKeyDown' | 'save' | 'cancel',
data?: SiteDto,
) => void,
readonly siteId: number | undefined,
Expand Down Expand Up @@ -120,6 +120,7 @@ const SiteModal = ({ open, handleClose, siteId }: Props) => {
<div className='mb-3'>
<label className='form-label text-capitalize' htmlFor='site-name'>
{t('analytics.site-modal.site-name')}
{site.siteName}
</label>
<input
className='form-control'
Expand Down Expand Up @@ -291,7 +292,7 @@ const SiteModal = ({ open, handleClose, siteId }: Props) => {
<DialogActions>
<button
className='btn btn-outline-primary'
onClick={() => handleClose()}
onClick={() => handleClose('cancel')}
type='button'
>
{t('generic.cancel')}
Expand Down
44 changes: 44 additions & 0 deletions canopeum_frontend/src/components/inputs/EmailTextField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { type FC, type InputHTMLAttributes, useState } from 'react'
import { useTranslation } from 'react-i18next'

import { isValidEmail } from '@utils/validators'

type Props = {
readonly value: string | undefined,
readonly attributes?: InputHTMLAttributes<HTMLInputElement>,
readonly onChange: (value: string) => void,
readonly isValid: (valid: boolean) => void,
}

const EmailTextField: FC<Props> = props => {
const { t } = useTranslation()
const [error, setError] = useState<string | null>(null)

const handleChange = (value: string) => {
if (value && !isValidEmail(value)) {
setError(t('errors.email-invalid'))
props.isValid(false)
} else {
setError(null)
props.isValid(true)
}

props.onChange(value)
}

return (
<div className='d-flex flex-column flex-grow-1'>
{/* eslint-disable react/jsx-props-no-spreading -- Needed for custom input */}
<input
{...props.attributes}
onChange={event => handleChange(event.target.value)}
type='email'
value={props.value}
/>
{/* eslint-enable react/jsx-props-no-spreading */}
{error && <span className='help-block text-danger'>{error}</span>}
</div>
)
}

export default EmailTextField
44 changes: 44 additions & 0 deletions canopeum_frontend/src/components/inputs/PhoneTextField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { type FC, type InputHTMLAttributes, useState } from 'react'
import { useTranslation } from 'react-i18next'

import { isValidPhone } from '@utils/validators'

type Props = {
readonly value: string | undefined,
readonly attributes?: InputHTMLAttributes<HTMLInputElement>,
readonly onChange: (value: string) => void,
readonly isValid: (valid: boolean) => void,
}

const PhoneTextField: FC<Props> = props => {
const { t } = useTranslation()
const [error, setError] = useState<string | null>(null)

const handleChange = (value: string) => {
if (value && !isValidPhone(value)) {
setError(t('errors.phone-invalid'))
props.isValid(false)
} else {
setError(null)
props.isValid(true)
}

props.onChange(value)
}

return (
<div className='d-flex flex-column flex-grow-1'>
{/* eslint-disable react/jsx-props-no-spreading -- Needed for custom input */}
<input
{...props.attributes}
onChange={event => handleChange(event.target.value)}
type='tel'
value={props.value}
/>
{/* eslint-enable react/jsx-props-no-spreading */}
{error && <span className='help-block text-danger'>{error}</span>}
</div>
)
}

export default PhoneTextField
44 changes: 44 additions & 0 deletions canopeum_frontend/src/components/inputs/UrlTextField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { type FC, type InputHTMLAttributes, useState } from 'react'
import { useTranslation } from 'react-i18next'

import { isValidUrl } from '@utils/validators'

type Props = {
readonly value: string | undefined,
readonly attributes?: InputHTMLAttributes<HTMLInputElement>,
readonly onChange: (value: string) => void,
readonly isValid: (valid: boolean) => void,
}

const UrlTextField: FC<Props> = props => {
const { t } = useTranslation()
const [error, setError] = useState<string | null>(null)

const handleChange = (value: string) => {
if (value && !isValidUrl(value)) {
setError(t('errors.url-invalid'))
props.isValid(false)
} else {
setError(null)
props.isValid(true)
}

props.onChange(value)
}

return (
<div className='d-flex flex-column flex-grow-1'>
{/* eslint-disable react/jsx-props-no-spreading -- Needed for custom input */}
<input
{...props.attributes}
onChange={event => handleChange(event.target.value)}
type='url'
value={props.value}
/>
{/* eslint-enable react/jsx-props-no-spreading */}
{error && <span className='help-block text-danger'>{error}</span>}
</div>
)
}

export default UrlTextField
57 changes: 57 additions & 0 deletions canopeum_frontend/src/components/social/AnnouncementCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { useState } from 'react'
import { Link } from 'react-router-dom'

import SiteAnnouncementModal from '@components/social/site-modal/SiteAnnouncementModal'
import type { PageViewMode } from '@models/types/PageViewMode.Type'
import type { Announcement } from '@services/api'

type Props = {
readonly announcement: Announcement,
readonly viewMode: PageViewMode,
readonly onEdit: (announcement: Announcement) => void,
}

const AnnouncementCard = ({ announcement, viewMode, onEdit }: Props) => {
const [isModalOpen, setIsModalOpen] = useState(false)

return (
<>
<div className='card rounded'>
<div className='card-body'>
<div className='d-flex justify-content-between align-items-center pb-3'>
<h2 className='card-title'>Announcement</h2>
<div>
{viewMode === 'admin' && (
<button
className='material-symbols-outlined text-primary fs-2'
onClick={() => setIsModalOpen(!isModalOpen)}
type='button'
>
edit_square
</button>
)}
</div>
</div>
<p className='card-text text-justify'>
{announcement.body}
</p>
{announcement.link && (
<Link className='card-text' target='_blank' to={{ pathname: announcement.link }}>
{announcement.link}
</Link>
)}
</div>
</div>
<SiteAnnouncementModal
announcement={announcement}
handleClose={(newAnnouncement: Announcement | null): void => {
setIsModalOpen(!isModalOpen)
if (newAnnouncement) onEdit(newAnnouncement)
}}
isOpen={isModalOpen}
/>
</>
)
}

export default AnnouncementCard
Loading

0 comments on commit 2647b45

Please sign in to comment.