"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:
louiscklaw
2025-06-03 17:04:11 +08:00
parent 24920fb313
commit 7610d80005
18 changed files with 867 additions and 246 deletions

View File

@@ -4,6 +4,8 @@ import { Speaker } from '../models/Speaker';
import { Location } from '../models/Location';
import axios from 'axios';
import constants from '../constants';
import { IOrderItem } from '../models/Order';
import { Event } from '../models/Event';
const dataUrl = '/assets/data/data.json';
const locationsUrl = '/assets/data/locations.json';
@@ -16,11 +18,13 @@ export const getConfData = async () => {
const response = await Promise.all([
fetch(dataUrl),
fetch(locationsUrl),
axios.get(`${constants.API_ENDPOINT}/api/order/list`),
fetch(`${constants.API_ENDPOINT}/api/order/list`),
fetch(`${constants.API_ENDPOINT}/api/event/list`),
// axios.get(`${constants.API_ENDPOINT}/v1/events`),
// axios.get(`${constants.API_ENDPOINT}/v1/members`),
//
]);
const responseData = await response[0].json();
const schedule = responseData.schedule[0] as Schedule;
const sessions = parseSessions(schedule);
@@ -33,9 +37,29 @@ export const getConfData = async () => {
// const events = response[2].data;
// const nearByMembers = response[3].data;
const orders = response[2].data.orders;
const events = [];
// TODO: update this due to not use axios anymore
// the data object is not available
// const orders = response[2].data.orders as IOrderItem[];
// const events = response[3].data.events as Event[];
const orderResponse = response[2];
let orders = {
result: { status: orderResponse.status, ok: orderResponse.ok },
data: [],
};
if (orderResponse.status == 200) {
orders = { ...orders, data: await orderResponse.json() };
}
const eventResponse = response[3];
let events = {
result: { status: eventResponse.status, ok: eventResponse.ok },
data: [],
};
if (eventResponse.status == 200) {
events = { ...events, data: await eventResponse.json() };
}
const nearByMembers = [];
const data = {
@@ -47,15 +71,21 @@ export const getConfData = async () => {
filteredTracks: [...allTracks],
//
events,
nearByMembers,
// nearByMembers,
orders,
hello: 'world',
//
};
return data;
};
export const getUserData = async () => {
const response = await Promise.all([Storage.get({ key: HAS_LOGGED_IN }), Storage.get({ key: HAS_SEEN_TUTORIAL }), Storage.get({ key: USERNAME })]);
const response = await Promise.all([
Storage.get({ key: HAS_LOGGED_IN }),
Storage.get({ key: HAS_SEEN_TUTORIAL }),
Storage.get({ key: USERNAME }),
]);
const isLoggedin = (await response[0].value) === 'true';
const hasSeenTutorial = (await response[1].value) === 'true';
const username = (await response[2].value) || undefined;

View File

@@ -2,9 +2,10 @@ import { createSelector } from 'reselect';
import { Schedule, Session, ScheduleGroup } from '../models/Schedule';
import { Speaker } from '../models/Speaker';
import { Location } from '../models/Location';
import { Event } from '../models/Event';
import { AppState } from './state';
import { IOrderItem } from '../models/Order';
import { Event } from '../models/Event';
const getSchedule = (state: AppState) => {
return state.data.schedule;
@@ -17,106 +18,126 @@ const getFilteredTracks = (state: AppState) => state.data.filteredTracks;
const getFavoriteIds = (state: AppState) => state.data.favorites;
const getSearchText = (state: AppState) => state.data.searchText;
export const getEvents = (state: AppState) => state.data.events;
export const getEvents = (state: AppState) => {
return state.data.events;
};
export const getNearbyMembers = (state: AppState) => state.data.nearByMembers;
export const getOrders = (state: AppState) => state.data.orders;
export const getFilteredSchedule = createSelector(getSchedule, getFilteredTracks, (schedule, filteredTracks) => {
const groups: ScheduleGroup[] = [];
export const getOrders = (state: AppState) => {
return state.data.orders;
};
// Helper function to convert 12-hour time to 24-hour time for proper sorting
const convertTo24Hour = (timeStr: string) => {
const [time, period] = timeStr.toLowerCase().split(' ');
let [hours, minutes] = time.split(':').map(Number);
export const getFilteredSchedule = createSelector(
getSchedule,
getFilteredTracks,
(schedule, filteredTracks) => {
const groups: ScheduleGroup[] = [];
if (period === 'pm' && hours !== 12) {
hours += 12;
} else if (period === 'am' && hours === 12) {
hours = 0;
}
// Helper function to convert 12-hour time to 24-hour time for proper sorting
const convertTo24Hour = (timeStr: string) => {
const [time, period] = timeStr.toLowerCase().split(' ');
let [hours, minutes] = time.split(':').map(Number);
return `${hours.toString().padStart(2, '0')}:${minutes || '00'}`;
};
if (period === 'pm' && hours !== 12) {
hours += 12;
} else if (period === 'am' && hours === 12) {
hours = 0;
}
// Sort the groups by time
const sortedGroups = [...schedule.groups].sort((a, b) => {
const timeA = convertTo24Hour(a.time);
const timeB = convertTo24Hour(b.time);
return timeA.localeCompare(timeB);
});
return `${hours.toString().padStart(2, '0')}:${minutes || '00'}`;
};
sortedGroups.forEach((group: ScheduleGroup) => {
const sessions: Session[] = [];
group.sessions.forEach((session) => {
session.tracks.forEach((track) => {
if (filteredTracks.indexOf(track) > -1) {
sessions.push(session);
}
});
// Sort the groups by time
const sortedGroups = [...schedule.groups].sort((a, b) => {
const timeA = convertTo24Hour(a.time);
const timeB = convertTo24Hour(b.time);
return timeA.localeCompare(timeB);
});
if (sessions.length) {
// Sort sessions within each group by start time
const sortedSessions = sessions.sort((a, b) => {
const timeA = convertTo24Hour(a.timeStart);
const timeB = convertTo24Hour(b.timeStart);
return timeA.localeCompare(timeB);
sortedGroups.forEach((group: ScheduleGroup) => {
const sessions: Session[] = [];
group.sessions.forEach((session) => {
session.tracks.forEach((track) => {
if (filteredTracks.indexOf(track) > -1) {
sessions.push(session);
}
});
});
const groupToAdd: ScheduleGroup = {
time: group.time,
sessions: sortedSessions,
};
groups.push(groupToAdd);
}
});
if (sessions.length) {
// Sort sessions within each group by start time
const sortedSessions = sessions.sort((a, b) => {
const timeA = convertTo24Hour(a.timeStart);
const timeB = convertTo24Hour(b.timeStart);
return timeA.localeCompare(timeB);
});
return {
date: schedule.date,
groups,
} as Schedule;
});
const groupToAdd: ScheduleGroup = {
time: group.time,
sessions: sortedSessions,
};
groups.push(groupToAdd);
}
});
export const getSearchedSchedule = createSelector(getFilteredSchedule, getSearchText, (schedule, searchText) => {
if (!searchText) {
return schedule;
return {
date: schedule.date,
groups,
} as Schedule;
}
const groups: ScheduleGroup[] = [];
schedule.groups.forEach((group) => {
const sessions = group.sessions.filter((s) => s.name.toLowerCase().indexOf(searchText.toLowerCase()) > -1);
if (sessions.length) {
const groupToAdd: ScheduleGroup = {
time: group.time,
sessions,
};
groups.push(groupToAdd);
);
export const getSearchedSchedule = createSelector(
getFilteredSchedule,
getSearchText,
(schedule, searchText) => {
if (!searchText) {
return schedule;
}
});
return {
date: schedule.date,
groups,
} as Schedule;
});
const groups: ScheduleGroup[] = [];
schedule.groups.forEach((group) => {
const sessions = group.sessions.filter(
(s) => s.name.toLowerCase().indexOf(searchText.toLowerCase()) > -1
);
if (sessions.length) {
const groupToAdd: ScheduleGroup = {
time: group.time,
sessions,
};
groups.push(groupToAdd);
}
});
return {
date: schedule.date,
groups,
} as Schedule;
}
);
export const getScheduleList = createSelector(getSearchedSchedule, (schedule) => schedule);
export const getGroupedFavorites = createSelector(getScheduleList, getFavoriteIds, (schedule, favoriteIds) => {
const groups: ScheduleGroup[] = [];
schedule.groups.forEach((group) => {
const sessions = group.sessions.filter((s) => favoriteIds.indexOf(s.id) > -1);
if (sessions.length) {
const groupToAdd: ScheduleGroup = {
time: group.time,
sessions,
};
groups.push(groupToAdd);
}
});
return {
date: schedule.date,
groups,
} as Schedule;
});
export const getGroupedFavorites = createSelector(
getScheduleList,
getFavoriteIds,
(schedule, favoriteIds) => {
const groups: ScheduleGroup[] = [];
schedule.groups.forEach((group) => {
const sessions = group.sessions.filter((s) => favoriteIds.indexOf(s.id) > -1);
if (sessions.length) {
const groupToAdd: ScheduleGroup = {
time: group.time,
sessions,
};
groups.push(groupToAdd);
}
});
return {
date: schedule.date,
groups,
} as Schedule;
}
);
const getIdParam = (_state: AppState, props: any) => {
return props.match.params['id'];
@@ -126,9 +147,25 @@ export const getSession = createSelector(getSessions, getIdParam, (sessions, id)
return sessions.find((s: Session) => s.id === id);
});
export const getSpeaker = createSelector(getSpeakers, getIdParam, (speakers, id) => speakers.find((x: Speaker) => x.id === id));
export const getSpeaker = createSelector(getSpeakers, getIdParam, (speakers, id) =>
speakers.find((x: Speaker) => x.id === id)
);
export const getEvent = createSelector(getEvents, getIdParam, (events, id) => events.find((x: Event) => x.id === id));
export const getEvent = createSelector(getEvents, getIdParam, (data_events, id) => {
const {
data: { events },
} = data_events;
return events.find((x: Event) => x.id === id);
});
export const getOrder = createSelector(getOrders, getIdParam, (data_orders, id) => {
const {
data: { orders },
} = data_orders;
return orders.find((x: IOrderItem) => x.id === id);
});
export const getSpeakerSessions = createSelector(getSessions, (sessions) => {
const speakerSessions: { [key: string]: Session[] } = {};

View File

@@ -3,6 +3,7 @@ import { Speaker } from '../../models/Speaker';
import { Schedule, Session } from '../../models/Schedule';
//
import { Event } from '../../models/Event';
import { IOrderItem } from '../../models/Order';
export interface ConfState {
schedule: Schedule;
@@ -18,4 +19,5 @@ export interface ConfState {
menuEnabled: boolean;
//
events: Event[];
orders: IOrderItem[];
}

View File

@@ -1,10 +1,7 @@
import { SessionsActions } from './sessions.actions';
import { ConfState } from './conf.state';
export const sessionsReducer = (
state: ConfState,
action: SessionsActions
): ConfState => {
export const sessionsReducer = (state: ConfState, action: SessionsActions): ConfState => {
switch (action.type) {
case 'set-conf-loading': {
return { ...state, loading: action.isLoading };

View File

@@ -31,23 +31,21 @@ export const logoutUser = () => async (dispatch: React.Dispatch<any>) => {
dispatch(setUsername());
};
export const setIsLoggedIn =
(loggedIn: boolean) => async (dispatch: React.Dispatch<any>) => {
await setIsLoggedInData(loggedIn);
return {
type: 'set-is-loggedin',
loggedIn,
} as const;
};
export const setIsLoggedIn = (loggedIn: boolean) => async (dispatch: React.Dispatch<any>) => {
await setIsLoggedInData(loggedIn);
return {
type: 'set-is-loggedin',
loggedIn,
} as const;
};
export const setUsername =
(username?: string) => async (dispatch: React.Dispatch<any>) => {
await setUsernameData(username);
return {
type: 'set-username',
username,
} as const;
};
export const setUsername = (username?: string) => async (dispatch: React.Dispatch<any>) => {
await setUsernameData(username);
return {
type: 'set-username',
username,
} as const;
};
export const setHasSeenTutorial =
(hasSeenTutorial: boolean) => async (dispatch: React.Dispatch<any>) => {

View File

@@ -1,8 +1,19 @@
// 03_source/mobile/src/models/Event.ts
export type IDateValue = string | number | null;
export interface Event {
eventDate: Date;
joinMembers: undefined;
title: string;
id: string;
createdAt: IDateValue;
updatedAt: IDateValue;
//
name: string;
code: string;
price: number;
//
eventDate: Date;
joinMembers: { email: string; avatar: string; sex: string }[];
title: string;
currency: string;
duration_m: number;
ageBottom: number;
@@ -10,5 +21,4 @@ export interface Event {
location: string;
avatar: string;
//
id: string;
}

View File

@@ -1,8 +1,42 @@
export type IDateValue = string | number | null;
export interface Order {
export type IOrderProductItem = {
id: string;
sku: string;
name: string;
price: number;
coverUrl: string;
quantity: number;
};
export type IOrderHistory = {
orderTime: IDateValue;
paymentTime: IDateValue;
deliveryTime: IDateValue;
completionTime: IDateValue;
timeline: { title: string; time: IDateValue }[];
};
export type IOrderDelivery = {
shipBy: string;
speedy: string;
trackingNumber: string;
};
export type IOrderShippingAddress = {
fullAddress: string;
phoneNumber: string;
};
export type IOrderPayment = {
cardType: string;
cardNumber: string;
};
export interface IOrderItem {
id: string;
createdAt: IDateValue;
updatedAt: IDateValue;
//
taxes: number;
status: string;
@@ -12,4 +46,10 @@ export interface Order {
orderNumber: string;
totalAmount: number;
totalQuantity: number;
//
items: IOrderProductItem[];
history: IOrderHistory | undefined;
delivery: IOrderDelivery;
shippingAddress: IOrderShippingAddress;
payment: IOrderPayment;
}

View 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;

View File

@@ -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,
});

View File

@@ -1,4 +1,4 @@
#about-page {
#event-detail-page {
ion-toolbar {
position: absolute;

View File

@@ -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>
</>
);
}

View 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>
</>
);
}

View 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>
</>
);
}

View File

@@ -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),
});

View File

@@ -1,4 +1,5 @@
export interface Event {
// OBSOLETED
export interface EventOBSOLETED {
eventDate: Date;
joinMembers: undefined;
title: string;

View 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),
});

View File

@@ -0,0 +1,2 @@
#order-detail-page {
}

View File

@@ -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),
});