Client - display segments - #14
This commit is contained in:
parent
86cb015279
commit
b2af180e05
@ -81,6 +81,22 @@ export const getActivityGpx = activityId => dispatch => {
|
|||||||
dispatch(setGpx(null))
|
dispatch(setGpx(null))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getSegmentGpx = (activityId, segmentId) => dispatch => {
|
||||||
|
if (activityId) {
|
||||||
|
return FitTrackeeGenericApi
|
||||||
|
.getData(`activities/${activityId}/gpx/segment/${segmentId}`)
|
||||||
|
.then(ret => {
|
||||||
|
if (ret.status === 'success') {
|
||||||
|
dispatch(setGpx(ret.data.gpx))
|
||||||
|
} else {
|
||||||
|
dispatch(setError(`activities: ${ret.message}`))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => dispatch(setError(`activities: ${error}`)))
|
||||||
|
}
|
||||||
|
dispatch(setGpx(null))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export const getActivityChartData = activityId => dispatch => {
|
export const getActivityChartData = activityId => dispatch => {
|
||||||
if (activityId) {
|
if (activityId) {
|
||||||
@ -98,6 +114,22 @@ export const getActivityChartData = activityId => dispatch => {
|
|||||||
dispatch(setChartData(null))
|
dispatch(setChartData(null))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getSegmentChartData = (activityId, segmentId) => dispatch => {
|
||||||
|
if (activityId) {
|
||||||
|
return FitTrackeeGenericApi
|
||||||
|
.getData(`activities/${activityId}/chart_data/segment/${segmentId}`)
|
||||||
|
.then(ret => {
|
||||||
|
if (ret.status === 'success') {
|
||||||
|
dispatch(setChartData(formatChartData(ret.data.chart_data)))
|
||||||
|
} else {
|
||||||
|
dispatch(setError(`activities: ${ret.message}`))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => dispatch(setError(`activities: ${error}`)))
|
||||||
|
}
|
||||||
|
dispatch(setChartData(null))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export const deleteActivity = id => dispatch => FitTrackeeGenericApi
|
export const deleteActivity = id => dispatch => FitTrackeeGenericApi
|
||||||
.deleteData('activities', id)
|
.deleteData('activities', id)
|
||||||
|
@ -6,32 +6,47 @@ import { formatActivityDate } from '../../../utils/activities'
|
|||||||
|
|
||||||
|
|
||||||
export default function ActivityCardHeader(props) {
|
export default function ActivityCardHeader(props) {
|
||||||
const { activity, displayModal, sport, title, user } = props
|
const {
|
||||||
|
activity, dataType, displayModal, segmentId, sport, title, user
|
||||||
|
} = props
|
||||||
const activityDate = activity
|
const activityDate = activity
|
||||||
? formatActivityDate(
|
? formatActivityDate(
|
||||||
getDateWithTZ(activity.activity_date, user.timezone)
|
getDateWithTZ(activity.activity_date, user.timezone)
|
||||||
)
|
)
|
||||||
: null
|
: null
|
||||||
|
|
||||||
|
const previousUrl = dataType === 'segment' && segmentId !== 0
|
||||||
|
? `/activities/${activity.id}/segment/${segmentId - 1}`
|
||||||
|
: dataType === 'activity' && activity.previous_activity
|
||||||
|
? `/activities/${activity.previous_activity}`
|
||||||
|
: null
|
||||||
|
const nextUrl =
|
||||||
|
dataType === 'segment' && segmentId < activity.segments.length - 1
|
||||||
|
? `/activities/${activity.id}/segment/${segmentId + 1}`
|
||||||
|
: dataType === 'activity' && activity.next_activity
|
||||||
|
? `/activities/${activity.next_activity}`
|
||||||
|
: null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container">
|
<div className="container">
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-auto">
|
<div className="col-auto">
|
||||||
{activity.previous_activity ? (
|
{previousUrl ? (
|
||||||
<Link
|
<Link
|
||||||
className="unlink"
|
className="unlink"
|
||||||
to={`/activities/${activity.previous_activity}`}
|
to={previousUrl}
|
||||||
>
|
>
|
||||||
<i
|
<i
|
||||||
className="fa fa-chevron-left"
|
className="fa fa-chevron-left"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
title="See previous activity"
|
title={`See previous ${dataType}`}
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
<i
|
<i
|
||||||
className="fa fa-chevron-left inactive-link"
|
className="fa fa-chevron-left inactive-link"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
title="No previous activity"
|
title={`No previous ${dataType}`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -43,6 +58,8 @@ export default function ActivityCardHeader(props) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="col">
|
<div className="col">
|
||||||
|
{dataType === 'activity' ? (
|
||||||
|
<>
|
||||||
{title}{' '}
|
{title}{' '}
|
||||||
<Link
|
<Link
|
||||||
className="unlink"
|
className="unlink"
|
||||||
@ -59,7 +76,19 @@ export default function ActivityCardHeader(props) {
|
|||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
onClick={() => displayModal(true)}
|
onClick={() => displayModal(true)}
|
||||||
title="Delete activity"
|
title="Delete activity"
|
||||||
/><br />
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Link
|
||||||
|
to={`/activities/${activity.id}`}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</Link>{' '}
|
||||||
|
- segment {segmentId + 1}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<br />
|
||||||
{activityDate && (
|
{activityDate && (
|
||||||
<span className="activity-date">
|
<span className="activity-date">
|
||||||
{`${activityDate.activity_date} - ${activityDate.activity_time}`}
|
{`${activityDate.activity_date} - ${activityDate.activity_time}`}
|
||||||
@ -67,22 +96,22 @@ export default function ActivityCardHeader(props) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="col-auto">
|
<div className="col-auto">
|
||||||
{activity.next_activity ? (
|
{nextUrl ? (
|
||||||
<Link
|
<Link
|
||||||
className="unlink"
|
className="unlink"
|
||||||
to={`/activities/${activity.next_activity}`}
|
to={nextUrl}
|
||||||
>
|
>
|
||||||
<i
|
<i
|
||||||
className="fa fa-chevron-right"
|
className="fa fa-chevron-right"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
title="See next activity"
|
title={`See next ${dataType}`}
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
<i
|
<i
|
||||||
className="fa fa-chevron-right inactive-link"
|
className="fa fa-chevron-right inactive-link"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
title="No next activity"
|
title={`No next ${dataType}`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -5,7 +5,9 @@ import {
|
|||||||
Area, ComposedChart, Line, ResponsiveContainer, Tooltip, XAxis, YAxis
|
Area, ComposedChart, Line, ResponsiveContainer, Tooltip, XAxis, YAxis
|
||||||
} from 'recharts'
|
} from 'recharts'
|
||||||
|
|
||||||
import { getActivityChartData } from '../../../actions/activities'
|
import {
|
||||||
|
getActivityChartData, getSegmentChartData
|
||||||
|
} from '../../../actions/activities'
|
||||||
|
|
||||||
|
|
||||||
class ActivityCharts extends React.Component {
|
class ActivityCharts extends React.Component {
|
||||||
@ -18,14 +20,24 @@ class ActivityCharts extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
if (this.props.dataType === 'activity') {
|
||||||
this.props.loadActivityData(this.props.activity.id)
|
this.props.loadActivityData(this.props.activity.id)
|
||||||
|
} else {
|
||||||
|
this.props.loadSegmentData(this.props.activity.id, this.props.segmentId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
if (prevProps.activity.id !==
|
if (this.props.dataType === 'activity' && (
|
||||||
this.props.activity.id) {
|
prevProps.activity.id !== this.props.activity.id)
|
||||||
|
) {
|
||||||
this.props.loadActivityData(this.props.activity.id)
|
this.props.loadActivityData(this.props.activity.id)
|
||||||
}
|
}
|
||||||
|
if (this.props.dataType === 'segment' && (
|
||||||
|
prevProps.segmentId !== this.props.segmentId)
|
||||||
|
) {
|
||||||
|
this.props.loadSegmentData(this.props.activity.id, this.props.segmentId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
@ -196,5 +208,8 @@ export default connect(
|
|||||||
loadActivityData: activityId => {
|
loadActivityData: activityId => {
|
||||||
dispatch(getActivityChartData(activityId))
|
dispatch(getActivityChartData(activityId))
|
||||||
},
|
},
|
||||||
|
loadSegmentData: (activityId, segmentId) => {
|
||||||
|
dispatch(getSegmentChartData(activityId, segmentId))
|
||||||
|
},
|
||||||
})
|
})
|
||||||
)(ActivityCharts)
|
)(ActivityCharts)
|
||||||
|
@ -5,7 +5,6 @@ import ActivityWeather from './ActivityWeather'
|
|||||||
export default function ActivityDetails(props) {
|
export default function ActivityDetails(props) {
|
||||||
const { activity } = props
|
const { activity } = props
|
||||||
const withPauses = activity.pauses !== '0:00:00' && activity.pauses !== null
|
const withPauses = activity.pauses !== '0:00:00' && activity.pauses !== null
|
||||||
const recordLDexists = activity.records.find(r => r.record_type === 'LD')
|
|
||||||
return (
|
return (
|
||||||
<div className="activity-details">
|
<div className="activity-details">
|
||||||
<p>
|
<p>
|
||||||
@ -14,7 +13,8 @@ export default function ActivityDetails(props) {
|
|||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
Duration: {activity.moving}
|
Duration: {activity.moving}
|
||||||
{recordLDexists && (
|
{activity.records && activity.records.find(r => r.record_type === 'LD'
|
||||||
|
) && (
|
||||||
<sup>
|
<sup>
|
||||||
<i
|
<i
|
||||||
className="fa fa-trophy custom-fa"
|
className="fa fa-trophy custom-fa"
|
||||||
@ -35,7 +35,7 @@ export default function ActivityDetails(props) {
|
|||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
Distance: {activity.distance} km
|
Distance: {activity.distance} km
|
||||||
{activity.records.find(r => r.record_type === 'FD'
|
{activity.records && activity.records.find(r => r.record_type === 'FD'
|
||||||
) && (
|
) && (
|
||||||
<sup>
|
<sup>
|
||||||
<i
|
<i
|
||||||
@ -51,7 +51,7 @@ export default function ActivityDetails(props) {
|
|||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
Average speed: {activity.ave_speed} km/h
|
Average speed: {activity.ave_speed} km/h
|
||||||
{activity.records.find(r => r.record_type === 'AS'
|
{activity.records && activity.records.find(r => r.record_type === 'AS'
|
||||||
) && (
|
) && (
|
||||||
<sup>
|
<sup>
|
||||||
<i
|
<i
|
||||||
@ -62,7 +62,7 @@ export default function ActivityDetails(props) {
|
|||||||
)}
|
)}
|
||||||
<br />
|
<br />
|
||||||
Max speed : {activity.max_speed} km/h
|
Max speed : {activity.max_speed} km/h
|
||||||
{activity.records.find(r => r.record_type === 'MS'
|
{activity.records && activity.records.find(r => r.record_type === 'MS'
|
||||||
) && (
|
) && (
|
||||||
<sup>
|
<sup>
|
||||||
<i
|
<i
|
||||||
|
@ -3,7 +3,7 @@ import React from 'react'
|
|||||||
import { GeoJSON, Map, Marker, TileLayer } from 'react-leaflet'
|
import { GeoJSON, Map, Marker, TileLayer } from 'react-leaflet'
|
||||||
import { connect } from 'react-redux'
|
import { connect } from 'react-redux'
|
||||||
|
|
||||||
import { getActivityGpx } from '../../../actions/activities'
|
import { getActivityGpx, getSegmentGpx } from '../../../actions/activities'
|
||||||
import { thunderforestApiKey } from '../../../utils'
|
import { thunderforestApiKey } from '../../../utils'
|
||||||
import { getGeoJson } from '../../../utils/activities'
|
import { getGeoJson } from '../../../utils/activities'
|
||||||
|
|
||||||
@ -17,14 +17,24 @@ class ActivityMap extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
if (this.props.dataType === 'activity') {
|
||||||
this.props.loadActivityGpx(this.props.activity.id)
|
this.props.loadActivityGpx(this.props.activity.id)
|
||||||
|
} else {
|
||||||
|
this.props.loadSegmentGpx(this.props.activity.id, this.props.segmentId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
if (prevProps.activity.id !==
|
if (this.props.dataType === 'activity' && (
|
||||||
this.props.activity.id) {
|
prevProps.activity.id !== this.props.activity.id)
|
||||||
|
) {
|
||||||
this.props.loadActivityGpx(this.props.activity.id)
|
this.props.loadActivityGpx(this.props.activity.id)
|
||||||
}
|
}
|
||||||
|
if (this.props.dataType === 'segment' && (
|
||||||
|
prevProps.segmentId !== this.props.segmentId)
|
||||||
|
) {
|
||||||
|
this.props.loadSegmentGpx(this.props.activity.id, this.props.segmentId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
@ -32,7 +42,9 @@ class ActivityMap extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { activity, coordinates, gpxContent } = this.props
|
const {
|
||||||
|
activity, coordinates, gpxContent
|
||||||
|
} = this.props
|
||||||
const { jsonData } = getGeoJson(gpxContent)
|
const { jsonData } = getGeoJson(gpxContent)
|
||||||
const bounds = [
|
const bounds = [
|
||||||
[activity.bounds[0], activity.bounds[1]],
|
[activity.bounds[0], activity.bounds[1]],
|
||||||
@ -79,5 +91,8 @@ export default connect(
|
|||||||
loadActivityGpx: activityId => {
|
loadActivityGpx: activityId => {
|
||||||
dispatch(getActivityGpx(activityId))
|
dispatch(getActivityGpx(activityId))
|
||||||
},
|
},
|
||||||
|
loadSegmentGpx: (activityId, segmentId) => {
|
||||||
|
dispatch(getSegmentGpx(activityId, segmentId))
|
||||||
|
},
|
||||||
})
|
})
|
||||||
)(ActivityMap)
|
)(ActivityMap)
|
||||||
|
@ -3,7 +3,9 @@ import React from 'react'
|
|||||||
export default function ActivityNotes(props) {
|
export default function ActivityNotes(props) {
|
||||||
const { notes } = props
|
const { notes } = props
|
||||||
return (
|
return (
|
||||||
<div className="card">
|
<div className="row">
|
||||||
|
<div className="col">
|
||||||
|
<div className="card activity-card">
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
Notes
|
Notes
|
||||||
<div className="activity-notes">
|
<div className="activity-notes">
|
||||||
@ -11,5 +13,7 @@ export default function ActivityNotes(props) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,33 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
|
||||||
|
export default function ActivitySegments(props) {
|
||||||
|
const { segments } = props
|
||||||
|
return (
|
||||||
|
<div className="row">
|
||||||
|
<div className="col">
|
||||||
|
<div className="card activity-card">
|
||||||
|
<div className="card-body">
|
||||||
|
Segments
|
||||||
|
<div className="activity-segments">
|
||||||
|
<ul>
|
||||||
|
{segments.map((segment, index) => (
|
||||||
|
// eslint-disable-next-line react/no-array-index-key
|
||||||
|
<li key={`segment-${index}`}>
|
||||||
|
<Link
|
||||||
|
to={`/activities/${
|
||||||
|
segment.activity_id}/segment/${index}`}
|
||||||
|
>
|
||||||
|
segment {index + 1}
|
||||||
|
</Link>{' '}
|
||||||
|
({segment.distance} km, duration: {segment.duration})
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -8,6 +8,7 @@ import ActivityDetails from './ActivityDetails'
|
|||||||
import ActivityMap from './ActivityMap'
|
import ActivityMap from './ActivityMap'
|
||||||
import ActivityNoMap from './ActivityNoMap'
|
import ActivityNoMap from './ActivityNoMap'
|
||||||
import ActivityNotes from './ActivityNotes'
|
import ActivityNotes from './ActivityNotes'
|
||||||
|
import ActivitySegments from './ActivitySegments'
|
||||||
import CustomModal from '../../Common/CustomModal'
|
import CustomModal from '../../Common/CustomModal'
|
||||||
import { getOrUpdateData } from '../../../actions'
|
import { getOrUpdateData } from '../../../actions'
|
||||||
import { deleteActivity } from '../../../actions/activities'
|
import { deleteActivity } from '../../../actions/activities'
|
||||||
@ -66,7 +67,10 @@ class ActivityDisplay extends React.Component {
|
|||||||
const [sport] = activity
|
const [sport] = activity
|
||||||
? sports.filter(s => s.id === activity.sport_id)
|
? sports.filter(s => s.id === activity.sport_id)
|
||||||
: []
|
: []
|
||||||
|
const segmentId = parseInt(this.props.match.params.segmentId)
|
||||||
|
const dataType = segmentId >= 0
|
||||||
|
? 'segment'
|
||||||
|
: 'activity'
|
||||||
return (
|
return (
|
||||||
<div className="activity-page">
|
<div className="activity-page">
|
||||||
<Helmet>
|
<Helmet>
|
||||||
@ -94,6 +98,8 @@ class ActivityDisplay extends React.Component {
|
|||||||
<div className="card-header">
|
<div className="card-header">
|
||||||
<ActivityCardHeader
|
<ActivityCardHeader
|
||||||
activity={activity}
|
activity={activity}
|
||||||
|
dataType={dataType}
|
||||||
|
segmentId={segmentId}
|
||||||
sport={sport}
|
sport={sport}
|
||||||
title={title}
|
title={title}
|
||||||
user={user}
|
user={user}
|
||||||
@ -107,13 +113,19 @@ class ActivityDisplay extends React.Component {
|
|||||||
<ActivityMap
|
<ActivityMap
|
||||||
activity={activity}
|
activity={activity}
|
||||||
coordinates={coordinates}
|
coordinates={coordinates}
|
||||||
|
dataType={dataType}
|
||||||
|
segmentId={segmentId}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ActivityNoMap />
|
<ActivityNoMap />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="col">
|
<div className="col">
|
||||||
<ActivityDetails activity={activity} />
|
<ActivityDetails
|
||||||
|
activity={dataType === 'activity'
|
||||||
|
? activity
|
||||||
|
: activity.segments[segmentId]}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -130,6 +142,8 @@ class ActivityDisplay extends React.Component {
|
|||||||
<div className="chart-title">Chart</div>
|
<div className="chart-title">Chart</div>
|
||||||
<ActivityCharts
|
<ActivityCharts
|
||||||
activity={activity}
|
activity={activity}
|
||||||
|
dataType={dataType}
|
||||||
|
segmentId={segmentId}
|
||||||
updateCoordinates={
|
updateCoordinates={
|
||||||
e => this.updateCoordinates(e)
|
e => this.updateCoordinates(e)
|
||||||
}
|
}
|
||||||
@ -141,7 +155,14 @@ class ActivityDisplay extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{dataType === 'activity' && (
|
||||||
|
<>
|
||||||
<ActivityNotes notes={activity.notes} />
|
<ActivityNotes notes={activity.notes} />
|
||||||
|
{activity.segments.length > 1 && (
|
||||||
|
<ActivitySegments segments={activity.segments} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -26,6 +26,10 @@ function Activity () {
|
|||||||
exact path="/activities/:activityId/edit"
|
exact path="/activities/:activityId/edit"
|
||||||
component={ActivityEdit}
|
component={ActivityEdit}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="/activities/:activityId/segment/:segmentId"
|
||||||
|
component={ActivityDisplay}
|
||||||
|
/>
|
||||||
<Route component={NotFound} />
|
<Route component={NotFound} />
|
||||||
</Switch>
|
</Switch>
|
||||||
) : (<Redirect to="/login" />)}
|
) : (<Redirect to="/login" />)}
|
||||||
|
@ -102,7 +102,7 @@ label {
|
|||||||
line-height: 400px;
|
line-height: 400px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.activity-notes {
|
.activity-notes, .actvitiy-segments {
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
|
Loading…
Reference in New Issue
Block a user