"feat: update data APIs to fetch orders and events via fetch instead of axios, add Order and Event models, update selectors and reducers, add EventDetail page with joined members display"
This commit is contained in:
134
03_source/mobile/src/pages/EventDetail/AvatarRow.tsx
Normal file
134
03_source/mobile/src/pages/EventDetail/AvatarRow.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
// REQ0042/event-detail
|
||||
//
|
||||
// PURPOSE:
|
||||
// - show avatar in a row
|
||||
//
|
||||
// RULES:
|
||||
// - T.B.A.
|
||||
//
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
IonHeader,
|
||||
IonToolbar,
|
||||
IonContent,
|
||||
IonPage,
|
||||
IonButtons,
|
||||
IonMenuButton,
|
||||
IonButton,
|
||||
IonIcon,
|
||||
IonDatetime,
|
||||
IonSelectOption,
|
||||
IonList,
|
||||
IonItem,
|
||||
IonLabel,
|
||||
IonSelect,
|
||||
IonPopover,
|
||||
IonText,
|
||||
IonFooter,
|
||||
useIonRouter,
|
||||
IonAvatar,
|
||||
IonThumbnail,
|
||||
} from '@ionic/react';
|
||||
import './style.scss';
|
||||
import {
|
||||
chevronBackOutline,
|
||||
ellipsisHorizontal,
|
||||
ellipsisVertical,
|
||||
heart,
|
||||
logoIonic,
|
||||
} from 'ionicons/icons';
|
||||
import AboutPopover from '../../components/AboutPopover';
|
||||
import { format, parseISO } from 'date-fns';
|
||||
import { TestContent } from './TestContent';
|
||||
import { Helloworld } from '../../api/Helloworld';
|
||||
import { getEventById } from '../../api/getEventById';
|
||||
import { connect } from '../../data/connect';
|
||||
import * as selectors from '../../data/selectors';
|
||||
import { Event } from '../../models/Event';
|
||||
import { RouteComponentProps } from 'react-router';
|
||||
|
||||
const leftShift: number = 10;
|
||||
const thumbnailSize: number = 40;
|
||||
|
||||
interface OwnProps extends RouteComponentProps {
|
||||
event_detail?: Event;
|
||||
}
|
||||
|
||||
interface StateProps {}
|
||||
|
||||
interface DispatchProps {}
|
||||
|
||||
interface EventDetailProps {
|
||||
avatars: string[];
|
||||
}
|
||||
|
||||
const AvatarRow: React.FC<{ avatars: string[] }> = ({ avatars }) => {
|
||||
const router = useIonRouter();
|
||||
|
||||
const [showPopover, setShowPopover] = useState(false);
|
||||
const [popoverEvent, setPopoverEvent] = useState<MouseEvent>();
|
||||
const [location, setLocation] = useState<'madison' | 'austin' | 'chicago' | 'seattle'>('madison');
|
||||
const [conferenceDate, setConferenceDate] = useState('2047-05-17T00:00:00-05:00');
|
||||
|
||||
const selectOptions = {
|
||||
header: 'Select a Location',
|
||||
};
|
||||
|
||||
const presentPopover = (e: React.MouseEvent) => {
|
||||
setPopoverEvent(e.nativeEvent);
|
||||
setShowPopover(true);
|
||||
};
|
||||
|
||||
function displayDate(date: string, dateFormat: string) {
|
||||
return format(parseISO(date), dateFormat);
|
||||
}
|
||||
|
||||
const [eventDetail, setEventDetail] = useState<Event | null>(null);
|
||||
useEffect(() => {
|
||||
Helloworld();
|
||||
getEventById('1').then(({ data }) => {
|
||||
console.log({ data });
|
||||
setEventDetail(data);
|
||||
});
|
||||
}, []);
|
||||
|
||||
function handleBackOnClick() {
|
||||
router.goBack();
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{ display: 'inline-flex', alignItems: 'center' }}>
|
||||
{avatars.slice(0, 3).map((m_avatar, idx) => (
|
||||
<div
|
||||
style={
|
||||
idx == 0
|
||||
? {}
|
||||
: {
|
||||
position: 'relative',
|
||||
width: `calc( ${thumbnailSize}px - ${leftShift}px )`,
|
||||
left: `-${leftShift}px`,
|
||||
}
|
||||
}
|
||||
>
|
||||
<IonThumbnail
|
||||
style={{
|
||||
'--size': `${thumbnailSize}px`,
|
||||
'--border-radius': `${thumbnailSize / 2}px`,
|
||||
border: '3px solid white',
|
||||
}}
|
||||
>
|
||||
<img alt="Silhouette of a person's head" src={m_avatar} />
|
||||
</IonThumbnail>
|
||||
</div>
|
||||
))}
|
||||
<div style={{ marginLeft: '0.1rem', fontWeight: 'bold' }}>
|
||||
{' '}
|
||||
+{avatars.length - 3} going{' '}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AvatarRow;
|
@@ -26,14 +26,31 @@ import {
|
||||
IonText,
|
||||
IonFooter,
|
||||
useIonRouter,
|
||||
IonAvatar,
|
||||
} from '@ionic/react';
|
||||
import './style.scss';
|
||||
import {
|
||||
accessibility,
|
||||
accessibilityOutline,
|
||||
chevronBackOutline,
|
||||
ellipsisHorizontal,
|
||||
ellipsisVertical,
|
||||
heart,
|
||||
locationOutline,
|
||||
locationSharp,
|
||||
logoIonic,
|
||||
man,
|
||||
manOutline,
|
||||
people,
|
||||
peopleOutline,
|
||||
timer,
|
||||
timerOutline,
|
||||
timerSharp,
|
||||
wallet,
|
||||
walletOutline,
|
||||
walletSharp,
|
||||
woman,
|
||||
womanOutline,
|
||||
} from 'ionicons/icons';
|
||||
import AboutPopover from '../../components/AboutPopover';
|
||||
import { format, parseISO } from 'date-fns';
|
||||
@@ -44,28 +61,46 @@ import { connect } from '../../data/connect';
|
||||
import * as selectors from '../../data/selectors';
|
||||
import { Event } from '../../models/Event';
|
||||
import { RouteComponentProps } from 'react-router';
|
||||
import AvatarRow from './AvatarRow';
|
||||
|
||||
const leftShift: number = -25;
|
||||
|
||||
interface OwnProps extends RouteComponentProps {
|
||||
event?: Event;
|
||||
event_detail?: Event;
|
||||
}
|
||||
|
||||
interface StateProps {}
|
||||
|
||||
interface DispatchProps {}
|
||||
|
||||
interface SpeakerDetailProps extends OwnProps, StateProps, DispatchProps {}
|
||||
interface EventDetailProps extends OwnProps, StateProps, DispatchProps {}
|
||||
|
||||
interface AboutProps {}
|
||||
const showJoinedMembers = (joinMembers: Record<string, any>[]) => {
|
||||
const avatars = joinMembers.map((jm) => jm.avatar);
|
||||
|
||||
console.log({ joinMembers });
|
||||
|
||||
return (
|
||||
<>
|
||||
<AvatarRow avatars={avatars} />
|
||||
<IonButton style={{ '--padding-start': '20px', '--padding-end': '20px' }} shape="round">
|
||||
More
|
||||
</IonButton>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const EventDetail: React.FC<EventDetailProps> = ({ event_detail }) => {
|
||||
const router = useIonRouter();
|
||||
|
||||
const EventDetail: React.FC<SpeakerDetailProps> = () => {
|
||||
const [showPopover, setShowPopover] = useState(false);
|
||||
const [popoverEvent, setPopoverEvent] = useState<MouseEvent>();
|
||||
const [location, setLocation] = useState<
|
||||
'madison' | 'austin' | 'chicago' | 'seattle'
|
||||
>('madison');
|
||||
const [conferenceDate, setConferenceDate] = useState(
|
||||
'2047-05-17T00:00:00-05:00'
|
||||
);
|
||||
const [location, setLocation] = useState<'madison' | 'austin' | 'chicago' | 'seattle'>('madison');
|
||||
const [conferenceDate, setConferenceDate] = useState('2047-05-17T00:00:00-05:00');
|
||||
|
||||
const [totalJoinMembers, setTotalJoinMembers] = useState<number>(0);
|
||||
const [maleMembers, setMaleMembers] = useState<number>(0);
|
||||
const [femaleMembers, setFemaleMembers] = useState<number>(0);
|
||||
|
||||
const selectOptions = {
|
||||
header: 'Select a Location',
|
||||
@@ -80,6 +115,14 @@ const EventDetail: React.FC<SpeakerDetailProps> = () => {
|
||||
return format(parseISO(date), dateFormat);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (event_detail) {
|
||||
setTotalJoinMembers(event_detail.joinMembers.length);
|
||||
setMaleMembers(event_detail.joinMembers.filter((m) => m.sex == 'M').length);
|
||||
setFemaleMembers(event_detail.joinMembers.filter((m) => m.sex == 'F').length);
|
||||
}
|
||||
}, [event_detail]);
|
||||
|
||||
const [eventDetail, setEventDetail] = useState<Event | null>(null);
|
||||
useEffect(() => {
|
||||
Helloworld();
|
||||
@@ -89,15 +132,14 @@ const EventDetail: React.FC<SpeakerDetailProps> = () => {
|
||||
});
|
||||
}, []);
|
||||
|
||||
const router = useIonRouter();
|
||||
function handleBackOnClick() {
|
||||
router.goBack();
|
||||
}
|
||||
|
||||
if (!eventDetail) return <>loading</>;
|
||||
if (!event_detail) return <>loading</>;
|
||||
|
||||
return (
|
||||
<IonPage id="about-page">
|
||||
<IonPage id="event-detail-page">
|
||||
<IonContent>
|
||||
<IonHeader className="ion-no-border">
|
||||
<IonToolbar>
|
||||
@@ -109,63 +151,110 @@ const EventDetail: React.FC<SpeakerDetailProps> = () => {
|
||||
</IonButtons>
|
||||
<IonButtons slot="end">
|
||||
<IonButton onClick={presentPopover}>
|
||||
<IonIcon
|
||||
slot="icon-only"
|
||||
ios={ellipsisHorizontal}
|
||||
md={ellipsisVertical}
|
||||
></IonIcon>
|
||||
<IonIcon slot="icon-only" ios={ellipsisHorizontal} md={ellipsisVertical}></IonIcon>
|
||||
</IonButton>
|
||||
</IonButtons>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<div className="about-header">
|
||||
<div className="about-image madison" style={{ opacity: 1 }}></div>
|
||||
</div>
|
||||
|
||||
<div>{eventDetail.avatar}</div>
|
||||
|
||||
<div>
|
||||
<div>{format(new Date(eventDetail.eventDate), 'yyyy-MM-dd')}</div>
|
||||
<h1>{eventDetail.title}</h1>
|
||||
|
||||
<div style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
|
||||
<div>members place holder</div>
|
||||
<IonButton shape="round">More</IonButton>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
marginBottom: '1rem',
|
||||
paddingTop: '1rem',
|
||||
borderBottom: '1px solid black',
|
||||
}}
|
||||
className="about-image madison"
|
||||
style={{ opacity: 1, backgroundImage: `url(${event_detail.avatar[0]})` }}
|
||||
></div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
||||
<div style={{ display: 'flex', gap: '1rem' }}>
|
||||
<div>{eventDetail.currency}</div>
|
||||
<div>{eventDetail.price}</div>
|
||||
per person
|
||||
|
||||
<div style={{ paddingLeft: '0.5rem', paddingRight: '0.5rem' }}>
|
||||
<div>
|
||||
<div style={{ paddingTop: '0.25rem', color: '#007AFF' }}>
|
||||
{format(new Date(event_detail.eventDate), 'EEE, dd MMM yyyy, hh:mm a')}
|
||||
</div>
|
||||
<div style={{ paddingTop: '0.25rem', fontSize: '1.8rem', fontWeight: '500' }}>
|
||||
{event_detail.title}
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: '1rem',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
{event_detail.joinMembers && event_detail.joinMembers.length > 0 ? (
|
||||
showJoinedMembers(event_detail.joinMembers)
|
||||
) : (
|
||||
<>join fast !</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
marginBottom: '1rem',
|
||||
paddingTop: '1rem',
|
||||
borderBottom: '1px solid gray',
|
||||
}}
|
||||
></div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
paddingLeft: '0.5rem',
|
||||
paddingRight: '0.5rem',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '1rem',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', gap: '0.5rem', alignItems: 'center' }}>
|
||||
<IonIcon icon={walletSharp} style={{ fontSize: '1.5rem' }}></IonIcon>
|
||||
<div style={{ display: 'flex', gap: '0.15rem', alignItems: 'center' }}>
|
||||
<div style={{ fontWeight: 'bold' }}>{event_detail.currency}</div>
|
||||
<div style={{ fontWeight: 'bold' }}>{event_detail.price}</div>
|
||||
per person
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>{eventDetail.duration_m}</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: '1rem' }}>
|
||||
<div>{eventDetail.ageBottom}</div>
|
||||
<div>{eventDetail.ageTop}</div>
|
||||
<div>years old</div>
|
||||
<div style={{ display: 'flex', gap: '0.5rem', alignItems: 'center' }}>
|
||||
<IonIcon icon={timerSharp} style={{ fontSize: '1.5rem' }}></IonIcon>
|
||||
<div style={{ display: 'flex', gap: '0.15rem', alignItems: 'center' }}>
|
||||
{event_detail.duration_m}
|
||||
<div>mins</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>{eventDetail.location}</div>
|
||||
<div style={{ display: 'flex', gap: '1rem' }}>
|
||||
<IonIcon icon={logoIonic}></IonIcon>
|
||||
<div>40</div>
|
||||
<div style={{ display: 'flex', gap: '0.5rem', alignItems: 'center' }}>
|
||||
<IonIcon icon={people} style={{ fontSize: '1.5rem' }}></IonIcon>
|
||||
<div style={{ display: 'flex', gap: '0.15rem', alignItems: 'center' }}>
|
||||
<div>{event_detail.ageBottom}</div>~<div>{event_detail.ageTop}</div>
|
||||
<div>years old</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<IonIcon icon={logoIonic}></IonIcon>
|
||||
<div>20</div>
|
||||
<div style={{ display: 'flex', gap: '0.5rem', alignItems: 'center' }}>
|
||||
<IonIcon icon={locationSharp} style={{ fontSize: '1.5rem' }}></IonIcon>
|
||||
{event_detail.location}
|
||||
</div>
|
||||
|
||||
<IonIcon icon={logoIonic}></IonIcon>
|
||||
<div>20</div>
|
||||
<div style={{ display: 'flex', gap: '0.5rem', alignItems: 'center' }}>
|
||||
<IonIcon
|
||||
icon={accessibility}
|
||||
style={{ fontSize: '1.5rem', color: 'rgb(139, 44, 245)' }}
|
||||
></IonIcon>
|
||||
<div>{totalJoinMembers}</div>
|
||||
|
||||
<IonIcon
|
||||
icon={man}
|
||||
style={{ fontSize: '1.5rem', color: 'rgb(67, 110, 205)' }}
|
||||
></IonIcon>
|
||||
<div>{maleMembers}</div>
|
||||
|
||||
<IonIcon
|
||||
icon={woman}
|
||||
style={{ fontSize: '1.5rem', color: 'rgb(235, 50, 35)' }}
|
||||
></IonIcon>
|
||||
<div>{femaleMembers}</div>
|
||||
</div>
|
||||
</div>
|
||||
</IonContent>
|
||||
@@ -197,8 +286,11 @@ const EventDetail: React.FC<SpeakerDetailProps> = () => {
|
||||
};
|
||||
|
||||
export default connect({
|
||||
mapStateToProps: (state, ownProps) => ({
|
||||
event: selectors.getEvent(state, ownProps),
|
||||
}),
|
||||
mapStateToProps: (state, ownProps) => {
|
||||
console.log({ t1: selectors.getEvents(state) });
|
||||
return {
|
||||
event_detail: selectors.getEvent(state, ownProps),
|
||||
};
|
||||
},
|
||||
component: EventDetail,
|
||||
});
|
||||
|
@@ -1,4 +1,4 @@
|
||||
#about-page {
|
||||
#event-detail-page {
|
||||
ion-toolbar {
|
||||
position: absolute;
|
||||
|
||||
|
@@ -0,0 +1,12 @@
|
||||
import { IonIcon } from '@ionic/react';
|
||||
import { woman } from 'ionicons/icons';
|
||||
import React from 'react';
|
||||
|
||||
export function NumOfFemaleMemberJoin({ joinMembers }) {
|
||||
return (
|
||||
<>
|
||||
<IonIcon icon={woman} style={{ fontSize: '1.1rem', color: 'rgb(235, 50, 35)' }}></IonIcon>
|
||||
<div>{joinMembers.filter((jm) => jm.sex == 'F').length}</div>
|
||||
</>
|
||||
);
|
||||
}
|
12
03_source/mobile/src/pages/EventList/NumOfMaleMemberJoin.tsx
Normal file
12
03_source/mobile/src/pages/EventList/NumOfMaleMemberJoin.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import { IonIcon } from '@ionic/react';
|
||||
import { man } from 'ionicons/icons';
|
||||
import React from 'react';
|
||||
|
||||
export function NumOfMaleMemberJoin({ joinMembers }) {
|
||||
return (
|
||||
<>
|
||||
<IonIcon icon={man} style={{ fontSize: '1.1rem', color: 'rgb(67, 110, 205)' }}></IonIcon>
|
||||
<div>{joinMembers.filter((jm) => jm.sex == 'M').length}</div>
|
||||
</>
|
||||
);
|
||||
}
|
15
03_source/mobile/src/pages/EventList/NumOfMemberJoin.tsx
Normal file
15
03_source/mobile/src/pages/EventList/NumOfMemberJoin.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { IonIcon } from '@ionic/react';
|
||||
import { accessibility } from 'ionicons/icons';
|
||||
import React from 'react';
|
||||
|
||||
export function NumOfMemberJoin({ joinMembers }) {
|
||||
return (
|
||||
<>
|
||||
<IonIcon
|
||||
icon={accessibility}
|
||||
style={{ fontSize: '1.1rem', color: 'rgb(139, 44, 245)' }}
|
||||
></IonIcon>
|
||||
<div>{joinMembers.length}</div>
|
||||
</>
|
||||
);
|
||||
}
|
@@ -1,6 +1,6 @@
|
||||
// REQ0041/home_discover_event_tab
|
||||
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import React, { useRef } from 'react';
|
||||
import {
|
||||
IonHeader,
|
||||
IonToolbar,
|
||||
@@ -8,54 +8,49 @@ import {
|
||||
IonContent,
|
||||
IonPage,
|
||||
IonButtons,
|
||||
IonMenuButton,
|
||||
IonGrid,
|
||||
IonRow,
|
||||
IonCol,
|
||||
useIonRouter,
|
||||
IonButton,
|
||||
IonIcon,
|
||||
IonPopover,
|
||||
IonAvatar,
|
||||
IonImg,
|
||||
IonItem,
|
||||
IonLabel,
|
||||
IonList,
|
||||
IonModal,
|
||||
IonSearchbar,
|
||||
useIonModal,
|
||||
IonInput,
|
||||
IonRefresher,
|
||||
IonRefresherContent,
|
||||
RefresherEventDetail,
|
||||
} from '@ionic/react';
|
||||
import SpeakerItem from '../../components/SpeakerItem';
|
||||
import { Speaker } from '../../models/Speaker';
|
||||
import { Session } from '../../models/Schedule';
|
||||
import { connect } from '../../data/connect';
|
||||
import * as selectors from '../../data/selectors';
|
||||
import '../SpeakerList.scss';
|
||||
import { getEvents } from '../../api/getEvents';
|
||||
import { format } from 'date-fns';
|
||||
import { Event } from './types';
|
||||
import { chevronDownCircleOutline, heart, menuOutline } from 'ionicons/icons';
|
||||
import AboutPopover from '../../components/AboutPopover';
|
||||
|
||||
// import { Event } from './types';
|
||||
import { chevronDownCircleOutline, menuOutline } from 'ionicons/icons';
|
||||
import Loading from '../../components/Loading';
|
||||
import { Event } from '../../models/Event';
|
||||
//
|
||||
import { NumOfMemberJoin } from './NumOfMemberJoin';
|
||||
import { NumOfMaleMemberJoin } from './NumOfMaleMemberJoin';
|
||||
import { NumOfFemaleMemberJoin } from './NumOfFemaleMemberJoin';
|
||||
|
||||
interface OwnProps {}
|
||||
|
||||
interface StateProps {
|
||||
events: Event[];
|
||||
fetchEventResult: any;
|
||||
}
|
||||
|
||||
interface DispatchProps {}
|
||||
|
||||
interface SpeakerListProps extends OwnProps, StateProps, DispatchProps {}
|
||||
|
||||
const EventList: React.FC<SpeakerListProps> = ({ events }) => {
|
||||
const EventList: React.FC<SpeakerListProps> = ({ fetchEventResult }) => {
|
||||
const router = useIonRouter();
|
||||
const modal = useRef<HTMLIonModalElement>(null);
|
||||
|
||||
const router = useIonRouter();
|
||||
const {
|
||||
result: { status },
|
||||
data: { events },
|
||||
} = fetchEventResult;
|
||||
|
||||
function handleShowPartyEventDetail(event_id: string) {
|
||||
router.push(`/event_detail/${event_id}`);
|
||||
@@ -68,6 +63,9 @@ const EventList: React.FC<SpeakerListProps> = ({ events }) => {
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
if (status != 200)
|
||||
return <>Error during fetching event list, check /events endpoint if working</>;
|
||||
|
||||
if (!events || events.length == 0) return <Loading />;
|
||||
|
||||
return (
|
||||
@@ -84,9 +82,14 @@ const EventList: React.FC<SpeakerListProps> = ({ events }) => {
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonContent fullscreen={true}>
|
||||
<IonContent className="ion-padding" fullscreen={true}>
|
||||
<IonRefresher slot="fixed" onIonRefresh={handleRefresh}>
|
||||
<IonRefresherContent pullingIcon={chevronDownCircleOutline} pullingText="Pull to refresh" refreshingSpinner="circles" refreshingText="Refreshing..."></IonRefresherContent>
|
||||
<IonRefresherContent
|
||||
pullingIcon={chevronDownCircleOutline}
|
||||
pullingText="Pull to refresh"
|
||||
refreshingSpinner="circles"
|
||||
refreshingText="Refreshing..."
|
||||
></IonRefresherContent>
|
||||
</IonRefresher>
|
||||
|
||||
<IonHeader collapse="condense">
|
||||
@@ -101,35 +104,43 @@ const EventList: React.FC<SpeakerListProps> = ({ events }) => {
|
||||
<IonCol size="12" size-md="6" key={idx}>
|
||||
<div
|
||||
style={{
|
||||
padding: '1rem',
|
||||
border: '1px solid black',
|
||||
border: '1px solid lightgrey',
|
||||
borderRadius: '1rem',
|
||||
}}
|
||||
onClick={() => handleShowPartyEventDetail(event.id)}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
backgroundImage: `url("https://plus.unsplash.com/premium_photo-1683121126477-17ef068309bc")`,
|
||||
backgroundImage: `url(${event.avatar[0]})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
height: '33vw',
|
||||
//
|
||||
borderRadius: '1rem 1rem 0 0',
|
||||
}}
|
||||
></div>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '1rem',
|
||||
//
|
||||
marginTop: '1rem',
|
||||
}}
|
||||
>
|
||||
<div>{format(new Date(event.eventDate), 'yyyy-MM-dd')}</div>
|
||||
<div>{event.title}</div>
|
||||
<div>{event.currency}</div>
|
||||
<div>{event.price}</div>
|
||||
<div>
|
||||
{40} {20} {20}
|
||||
<div style={{ marginTop: '1rem' }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '0.5rem',
|
||||
padding: '0.5rem',
|
||||
paddingBottom: '1rem',
|
||||
}}
|
||||
>
|
||||
{/* <div>{format(new Date(event.eve ntDate), 'yyyy-MM-dd')}</div> */}
|
||||
<div style={{ color: 'rgb(0, 122, 255)' }}>
|
||||
{format(new Date(event.eventDate), 'EEE, dd MMM yyyy, hh:mm a')}
|
||||
</div>
|
||||
<div style={{ fontSize: '1.2rem', fontWeight: 'bold' }}>{event.name}</div>
|
||||
<div style={{ fontSize: '1.2rem', fontWeight: 'bold' }}>{event.price}</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: '0.25rem', alignItems: 'center' }}>
|
||||
<NumOfMemberJoin joinMembers={event.joinMembers} />
|
||||
<NumOfMaleMemberJoin joinMembers={event.joinMembers} />
|
||||
<NumOfFemaleMemberJoin joinMembers={event.joinMembers} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -140,7 +151,12 @@ const EventList: React.FC<SpeakerListProps> = ({ events }) => {
|
||||
</IonContent>
|
||||
|
||||
{/* REQ0079/event-filter */}
|
||||
<IonModal ref={modal} trigger="events-open-modal" initialBreakpoint={0.5} breakpoints={[0, 0.25, 0.5, 0.75]}>
|
||||
<IonModal
|
||||
ref={modal}
|
||||
trigger="events-open-modal"
|
||||
initialBreakpoint={0.5}
|
||||
breakpoints={[0, 0.25, 0.5, 0.75]}
|
||||
>
|
||||
<IonContent className="ion-padding">
|
||||
<div
|
||||
style={{
|
||||
@@ -203,8 +219,10 @@ const EventList: React.FC<SpeakerListProps> = ({ events }) => {
|
||||
};
|
||||
|
||||
export default connect<OwnProps, StateProps, DispatchProps>({
|
||||
mapStateToProps: (state) => ({
|
||||
events: selectors.getEvents(state),
|
||||
}),
|
||||
mapStateToProps: (state) => {
|
||||
return {
|
||||
fetchEventResult: selectors.getEvents(state),
|
||||
};
|
||||
},
|
||||
component: React.memo(EventList),
|
||||
});
|
||||
|
@@ -1,4 +1,5 @@
|
||||
export interface Event {
|
||||
// OBSOLETED
|
||||
export interface EventOBSOLETED {
|
||||
eventDate: Date;
|
||||
joinMembers: undefined;
|
||||
title: string;
|
||||
|
173
03_source/mobile/src/pages/OrderDetail/index.tsx
Normal file
173
03_source/mobile/src/pages/OrderDetail/index.tsx
Normal file
@@ -0,0 +1,173 @@
|
||||
// REQ0041/home_discover_event_tab
|
||||
|
||||
import {
|
||||
IonPage,
|
||||
IonHeader,
|
||||
IonToolbar,
|
||||
IonButtons,
|
||||
IonButton,
|
||||
IonIcon,
|
||||
IonTitle,
|
||||
IonContent,
|
||||
useIonRouter,
|
||||
} from '@ionic/react';
|
||||
import { chevronBackOutline, menuOutline } from 'ionicons/icons';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { useParams } from 'react-router';
|
||||
import { IOrderItem } from '../../models/Order';
|
||||
import { connect } from '../../data/connect';
|
||||
import * as selectors from '../../data/selectors';
|
||||
|
||||
import './style.scss';
|
||||
import paths from '../../paths';
|
||||
|
||||
interface OwnProps {}
|
||||
|
||||
interface StateProps {
|
||||
order: IOrderItem;
|
||||
}
|
||||
|
||||
interface DispatchProps {}
|
||||
|
||||
interface OrderDetailProps extends OwnProps, StateProps, DispatchProps {}
|
||||
|
||||
const OrderDetail: React.FC<OrderDetailProps> = ({ order }) => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const router = useIonRouter();
|
||||
|
||||
function handleBackClick() {
|
||||
router.goBack();
|
||||
}
|
||||
|
||||
return (
|
||||
<IonPage id="speaker-list">
|
||||
<IonHeader translucent={true} className="ion-no-border">
|
||||
<IonToolbar>
|
||||
<IonButtons slot="start">
|
||||
{/* <IonMenuButton /> */}
|
||||
<IonButton
|
||||
shape="round"
|
||||
id="events-open-modal"
|
||||
expand="block"
|
||||
onClick={handleBackClick}
|
||||
>
|
||||
<IonIcon slot="icon-only" icon={chevronBackOutline}></IonIcon>
|
||||
</IonButton>
|
||||
</IonButtons>
|
||||
<IonTitle>Order Details (訂單詳情)</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonContent className="ion-padding" fullscreen={true}>
|
||||
<div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<div>Total</div>
|
||||
<div>{order.totalAmount}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<div>created at:</div>
|
||||
<div>{order.createdAt}</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<div>updated at:</div>
|
||||
<div>{order.updatedAt}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>History</h2>
|
||||
<h3>Delivery</h3>
|
||||
<div style={{ display: 'flex', gap: '1rem' }}>
|
||||
<div>
|
||||
{order.history?.timeline.map((t) => (
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<div>{t.title}</div>
|
||||
<div>{t.time}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3>擇要</h3>
|
||||
<div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<div>Order time</div>
|
||||
<div>29 May 2025 4:01 pm</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<div>Payment time</div>
|
||||
<div>29 May 2025 4:01 pm</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<div>Delivery time for the carrier</div>
|
||||
<div>29 May 2025 4:01 pm</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<div>Completion time</div>
|
||||
<div>29 May 2025 4:01 pm</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2>Delivery</h2>
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<div>Ship by</div>
|
||||
<div>{order.delivery.shipBy}</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<div>Speedy</div>
|
||||
<div>{order.delivery.speedy}</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<div>Tracking No.</div>
|
||||
<div>{order.delivery.trackingNumber}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2>Shipping</h2>
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<div>Address</div>
|
||||
<div>{order.shippingAddress.fullAddress}</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<div>Phone Number</div>
|
||||
<div>{order.shippingAddress.phoneNumber}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2>Payment</h2>
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<div>Card Type</div>
|
||||
<div>{order.payment.cardType}</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<div>Card Number</div>
|
||||
<div>{order.payment.cardNumber}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default connect({
|
||||
mapStateToProps: (state, ownProps) => ({
|
||||
order: selectors.getOrder(state, ownProps),
|
||||
}),
|
||||
component: React.memo(OrderDetail),
|
||||
});
|
2
03_source/mobile/src/pages/OrderDetail/style.scss
Normal file
2
03_source/mobile/src/pages/OrderDetail/style.scss
Normal file
@@ -0,0 +1,2 @@
|
||||
#order-detail-page {
|
||||
}
|
@@ -31,7 +31,7 @@ import {
|
||||
} from '@ionic/react';
|
||||
import SpeakerItem from '../../components/SpeakerItem';
|
||||
import { Speaker } from '../../models/Speaker';
|
||||
import { Order } from '../../models/Order';
|
||||
import { IOrderItem } from '../../models/Order';
|
||||
import { Session } from '../../models/Schedule';
|
||||
import { connect } from '../../data/connect';
|
||||
import * as selectors from '../../data/selectors';
|
||||
@@ -39,7 +39,15 @@ import '../SpeakerList.scss';
|
||||
import { getEvents } from '../../api/getEvents';
|
||||
import { format } from 'date-fns';
|
||||
// import { Order } from './types';
|
||||
import { bookmarksOutline, chevronBackOutline, chevronDownCircleOutline, chevronForwardOutline, heart, logoIonic, menuOutline } from 'ionicons/icons';
|
||||
import {
|
||||
bookmarksOutline,
|
||||
chevronBackOutline,
|
||||
chevronDownCircleOutline,
|
||||
chevronForwardOutline,
|
||||
heart,
|
||||
logoIonic,
|
||||
menuOutline,
|
||||
} from 'ionicons/icons';
|
||||
import AboutPopover from '../../components/AboutPopover';
|
||||
import { getOrders } from '../../api/getOrders';
|
||||
import Loading from '../../components/Loading';
|
||||
@@ -48,7 +56,7 @@ import paths from '../../paths';
|
||||
interface OwnProps {}
|
||||
|
||||
interface StateProps {
|
||||
orders: Order[];
|
||||
fetchOrderResult: { result: { status: number; ok: boolean }; data: IOrderItem[] };
|
||||
//
|
||||
speakerSessions: { [key: string]: Session[] };
|
||||
}
|
||||
@@ -78,9 +86,19 @@ const NumApplicants: React.FC<{ amount: number }> = ({ amount }) => {
|
||||
const TotalAmount: React.FC<{ amount: number }> = ({ amount }) => {
|
||||
return (
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: '1.1rem' }}>
|
||||
<div style={{ display: 'flex', gap: '1rem', fontWeight: 'bold', fontSize: '1.2rem', opacity: 0.8 }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: '1rem',
|
||||
fontWeight: 'bold',
|
||||
fontSize: '1.2rem',
|
||||
opacity: 0.8,
|
||||
}}
|
||||
>
|
||||
<div>Total:</div>
|
||||
<div style={{ minWidth: '75px', display: 'inline-flex', justifyContent: 'flex-end' }}>{amount} </div>
|
||||
<div style={{ minWidth: '75px', display: 'inline-flex', justifyContent: 'flex-end' }}>
|
||||
{amount}{' '}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -91,7 +109,9 @@ const Subtotal: React.FC<{ amount: number }> = ({ amount }) => {
|
||||
return (
|
||||
<div style={{ display: 'flex', gap: '0.5rem', justifyContent: 'flex-end' }}>
|
||||
<div>Subtotal:</div>
|
||||
<div style={{ minWidth: '50px', display: 'inline-flex', justifyContent: 'flex-end' }}>{amount} </div>
|
||||
<div style={{ minWidth: '50px', display: 'inline-flex', justifyContent: 'flex-end' }}>
|
||||
{amount}{' '}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -102,7 +122,9 @@ const Shipping: React.FC<{ amount: number }> = ({ amount }) => {
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<div style={{ display: 'flex', gap: '1rem', fontWeight: 'bold' }}>
|
||||
<div>Shipping:</div>
|
||||
<div style={{ minWidth: '50px', display: 'inline-flex', justifyContent: 'flex-end' }}>{amount} </div>
|
||||
<div style={{ minWidth: '50px', display: 'inline-flex', justifyContent: 'flex-end' }}>
|
||||
{amount}{' '}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -114,7 +136,9 @@ const Discount: React.FC<{ amount: number }> = ({ amount }) => {
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<div style={{ display: 'flex', gap: '1rem', fontWeight: 'bold' }}>
|
||||
<div>Discount:</div>
|
||||
<div style={{ minWidth: '50px', display: 'inline-flex', justifyContent: 'flex-end' }}>{amount} </div>
|
||||
<div style={{ minWidth: '50px', display: 'inline-flex', justifyContent: 'flex-end' }}>
|
||||
{amount}{' '}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -126,25 +150,25 @@ const Tax: React.FC<{ amount: number }> = ({ amount }) => {
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<div style={{ display: 'flex', gap: '1rem', fontWeight: 'bold' }}>
|
||||
<div>Tax:</div>
|
||||
<div style={{ minWidth: '50px', display: 'inline-flex', justifyContent: 'flex-end' }}>{amount} </div>
|
||||
<div style={{ minWidth: '50px', display: 'inline-flex', justifyContent: 'flex-end' }}>
|
||||
{amount}{' '}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const EventList: React.FC<SpeakerListProps> = ({ orders, speakerSessions }) => {
|
||||
const OrderList: React.FC<SpeakerListProps> = ({ fetchOrderResult, speakerSessions }) => {
|
||||
const router = useIonRouter();
|
||||
|
||||
const [showPopover, setShowPopover] = useState(false);
|
||||
const [popoverEvent, setPopoverEvent] = useState<MouseEvent>();
|
||||
const modal = useRef<HTMLIonModalElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// getOrders().then(({ data }) => {
|
||||
// console.log({ data });
|
||||
// setEvents(data);
|
||||
// });
|
||||
}, []);
|
||||
const {
|
||||
result: { status },
|
||||
data: { orders },
|
||||
} = fetchOrderResult;
|
||||
|
||||
function handleRefresh(event: CustomEvent<RefresherEventDetail>) {
|
||||
setTimeout(() => {
|
||||
@@ -165,8 +189,13 @@ const EventList: React.FC<SpeakerListProps> = ({ orders, speakerSessions }) => {
|
||||
router.push(paths.FAVOURITES_LIST);
|
||||
}
|
||||
|
||||
if (status != 200)
|
||||
return <>Error during fetching order list, check /orders endpoint if working</>;
|
||||
|
||||
if (!orders) return <Loading />;
|
||||
|
||||
if (orders.length == 0) return <>order list is empty</>;
|
||||
|
||||
return (
|
||||
<IonPage id="speaker-list">
|
||||
<IonHeader translucent={true} className="ion-no-border">
|
||||
@@ -183,7 +212,12 @@ const EventList: React.FC<SpeakerListProps> = ({ orders, speakerSessions }) => {
|
||||
|
||||
<IonContent fullscreen={true}>
|
||||
<IonRefresher slot="fixed" onIonRefresh={handleRefresh}>
|
||||
<IonRefresherContent pullingIcon={chevronDownCircleOutline} pullingText="Pull to refresh" refreshingSpinner="circles" refreshingText="Refreshing..."></IonRefresherContent>
|
||||
<IonRefresherContent
|
||||
pullingIcon={chevronDownCircleOutline}
|
||||
pullingText="Pull to refresh"
|
||||
refreshingSpinner="circles"
|
||||
refreshingText="Refreshing..."
|
||||
></IonRefresherContent>
|
||||
</IonRefresher>
|
||||
|
||||
<IonHeader collapse="condense">
|
||||
@@ -193,17 +227,27 @@ const EventList: React.FC<SpeakerListProps> = ({ orders, speakerSessions }) => {
|
||||
</IonHeader>
|
||||
|
||||
<IonList>
|
||||
{orders.map((order, idx) => (
|
||||
<IonItem button onClick={handleNotImplemented}>
|
||||
{orders.map((order: IOrderItem, idx: number) => (
|
||||
<IonItem button onClick={() => handleShowOrderDetail(order.id)} key={idx}>
|
||||
<div style={{ paddingBottom: '1rem', paddingTop: '1rem' }}>
|
||||
<div style={{ display: 'flex', gap: '0.5rem', width: 'calc( 100vw - 35px )' }}>
|
||||
<div style={{}}>
|
||||
<div>
|
||||
<div style={{ width: '70px' }}>
|
||||
<IonAvatar>
|
||||
<img alt="Silhouette of a person's head" src="https://plus.unsplash.com/premium_photo-1683121126477-17ef068309bc" />
|
||||
<img
|
||||
alt="Silhouette of a person's head"
|
||||
src="https://plus.unsplash.com/premium_photo-1683121126477-17ef068309bc"
|
||||
/>
|
||||
</IonAvatar>
|
||||
<div style={{ marginTop: '1rem', display: 'inline-flex', flexDirection: 'column', gap: '0.5rem' }}>
|
||||
<div
|
||||
style={{
|
||||
marginTop: '1rem',
|
||||
display: 'inline-flex',
|
||||
flexDirection: 'column',
|
||||
gap: '0.5rem',
|
||||
}}
|
||||
>
|
||||
<NumApplicants amount={38} />
|
||||
<RemainingDays amount={50} />
|
||||
</div>
|
||||
@@ -228,8 +272,12 @@ const EventList: React.FC<SpeakerListProps> = ({ orders, speakerSessions }) => {
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: '1.2rem' }}>{order.orderNumber}</div>
|
||||
<IonButton shape="round" onClick={() => handleShowOrderDetail('1')} size="small" fill="clear">
|
||||
<IonIcon slot="icon-only" icon={chevronForwardOutline} size="small"></IonIcon>
|
||||
<IonButton shape="round" size="small" fill="clear">
|
||||
<IonIcon
|
||||
slot="icon-only"
|
||||
icon={chevronForwardOutline}
|
||||
size="small"
|
||||
></IonIcon>
|
||||
</IonButton>
|
||||
</div>
|
||||
|
||||
@@ -272,9 +320,9 @@ const EventList: React.FC<SpeakerListProps> = ({ orders, speakerSessions }) => {
|
||||
|
||||
export default connect<OwnProps, StateProps, DispatchProps>({
|
||||
mapStateToProps: (state) => ({
|
||||
orders: selectors.getOrders(state),
|
||||
// TODO: review below
|
||||
fetchOrderResult: selectors.getOrders(state),
|
||||
// TODO: review unused code
|
||||
speakerSessions: selectors.getSpeakerSessions(state),
|
||||
}),
|
||||
component: React.memo(EventList),
|
||||
component: React.memo(OrderList),
|
||||
});
|
||||
|
Reference in New Issue
Block a user