"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:
@@ -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;
|
||||
|
@@ -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[] } = {};
|
||||
|
@@ -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[];
|
||||
}
|
||||
|
@@ -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 };
|
||||
|
@@ -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>) => {
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
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