feat: add CarousellMe feature with profile, listings and reviews components

This commit is contained in:
louiscklaw
2025-06-19 17:45:45 +08:00
parent 1fdf10c0da
commit 360da364ff
23 changed files with 1431 additions and 3 deletions

View File

@@ -62,6 +62,7 @@
"@types/react-dom": "^18.0.11",
"@types/react-router": "^5.1.20",
"@types/react-router-dom": "^5.3.3",
"@types/react-star-ratings": "^2.3.3",
"@vitejs/plugin-react": "^3.1.0",
"husky": "^8.0.3",
"lint-staged": "^13.2.0",
@@ -1326,6 +1327,16 @@
"@types/react-router": "*"
}
},
"node_modules/@types/react-star-ratings": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/@types/react-star-ratings/-/react-star-ratings-2.3.3.tgz",
"integrity": "sha512-8vLqJG1uRA2SmYBBMPWpv6QWHLvZ/a3XmELCIf4xh4VFXT7QkkrcthiLSMjQ4ibDiUtYYpyLB0JoR1lBHSHA2A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/react": "*"
}
},
"node_modules/@types/slice-ansi": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@types/slice-ansi/-/slice-ansi-4.0.0.tgz",
@@ -7054,6 +7065,15 @@
"@types/react-router": "*"
}
},
"@types/react-star-ratings": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/@types/react-star-ratings/-/react-star-ratings-2.3.3.tgz",
"integrity": "sha512-8vLqJG1uRA2SmYBBMPWpv6QWHLvZ/a3XmELCIf4xh4VFXT7QkkrcthiLSMjQ4ibDiUtYYpyLB0JoR1lBHSHA2A==",
"dev": true,
"requires": {
"@types/react": "*"
}
},
"@types/slice-ansi": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@types/slice-ansi/-/slice-ansi-4.0.0.tgz",

View File

@@ -61,6 +61,7 @@
"@types/react-dom": "^18.0.11",
"@types/react-router": "^5.1.20",
"@types/react-router-dom": "^5.3.3",
"@types/react-star-ratings": "^2.3.3",
"@vitejs/plugin-react": "^3.1.0",
"husky": "^8.0.3",
"lint-staged": "^13.2.0",

View File

@@ -70,6 +70,7 @@ import DummyPayPage from './pages/DummyEventPayPage';
import DummyEventPayPage from './pages/DummyEventPayPage';
import PaymentSuccess from './pages/PaymentSuccess';
import PaymentFailed from './pages/PaymentFailed';
import CarousellMe from './pages/CarousellMe';
setupIonicReact();

View File

@@ -23,11 +23,11 @@ const PATHS = {
getOrderDetail: (id: string) => `/order_detail/${id}`,
// Tab navigation routes
TAB_NOT_IMPLEMENTED: '/tabs/not_implemented',
EVENT_LIST: `/tabs/events`,
MESSAGE_LIST: `/tabs/messages`,
EVENT_LIST: '/tabs/events',
MESSAGE_LIST: '/tabs/messages',
NEARBY_LIST: '/tabs/nearby',
ORDERS_LIST: '/tabs/orders',
FAVOURITES_LIST: `/tabs/favourites`,
FAVOURITES_LIST: '/tabs/favourites',
PROFILE: '/tabs/my_profile',
// partyUser
@@ -102,5 +102,8 @@ const PATHS = {
DEMO_SLIDING_PROFILE: '/demo-sliding-profile',
DEMO_STICKY_BOTTOM_SHEET_EXAMPLE: '/demo-sticky-bottom-sheet-example',
DEMO_STORAGE_EXAMPLE: '/demo-storage-example',
//
CAROUSELL_ME: '/tabs/carousell_me',
};
export default PATHS;

View File

@@ -1,14 +1,23 @@
import React, { createContext, PropsWithChildren, useReducer } from 'react';
import { initialState, AppState, reducers } from './state';
import { Storage, Drivers } from '@ionic/storage';
const browser_store = new Storage({
name: '__mydb',
driverOrder: [Drivers.LocalStorage, Drivers.IndexedDB],
});
export interface AppContextState {
state: AppState;
dispatch: React.Dispatch<any>;
browser_store: any;
}
export const AppContext = createContext<AppContextState>({
state: initialState,
dispatch: () => undefined,
browser_store: {},
});
export const AppContextProvider: React.FC<PropsWithChildren> = ({ children }) => {
@@ -19,9 +28,13 @@ export const AppContextProvider: React.FC<PropsWithChildren> = ({ children }) =>
value={{
state: store,
dispatch,
//
browser_store,
}}
>
{children}
</AppContext.Provider>
);
};
//

View File

@@ -0,0 +1,130 @@
import React, { useState } from 'react';
// import { useTranslation } from 'react-i18next';
// import StarRatings from 'react-star-ratings';
import {
IonHeader,
IonTitle,
IonToolbar,
IonContent,
IonPage,
IonButtons,
IonBadge,
IonMenuButton,
IonButton,
IonIcon,
IonDatetime,
IonSelectOption,
IonList,
IonItem,
IonLabel,
IonSelect,
IonPopover,
IonText,
IonCard,
IonCardHeader,
IonCardTitle,
IonCardSubtitle,
IonCardContent,
IonTabs,
IonTabBar,
IonTabButton,
IonSegment,
IonSegmentButton,
useIonRouter,
} from '@ionic/react';
import './style.scss';
import {
locationSharp,
settingsOutline,
qrCode,
shareSocialOutline,
ellipsisHorizontal,
ellipsisVertical,
logoFacebook,
phonePortraitSharp,
pricetagSharp,
archiveSharp,
calendar,
personCircle,
map,
informationCircle,
mailSharp,
chevronDownSharp,
chevronForwardSharp,
} from 'ionicons/icons';
// import AboutPopover from '../../components/AboutPopover';
import { format, parseISO } from 'date-fns';
import lookForVisitorSvg from './look_for_visitor.svg';
type SessionListProps = {
hide: boolean;
};
const AboutContent: React.FC<SessionListProps> = ({ hide }) => {
let route = useIonRouter();
if (hide) return <></>;
return (
<>
<div
style={{
//
paddingTop: '1rem',
paddingLeft: '1rem',
paddingRight: '1rem',
display: 'flex',
flexDirection: 'column',
gap: '1rem',
}}
>
<div>
<IonText color="tertiary" style={{ fontSize: '0.8rem' }}>
<IonText style={{ fontWeight: 'bold' }} color="success">
25
</IonText>
<IonText>Followers</IonText>
{' '}
<IonText style={{ fontWeight: 'bold' }} color="success">
0
</IonText>
<IonText>Followers</IonText>
</IonText>
</div>
<div
style={{
paddingBottom: '3rem',
}}
>
<IonText style={{ fontSize: '0.9rem' }}>
<p> payme / / FPS</p>
<p>Accepting HSBC payme / alipay / FPS </p>
<p> / for more detail:</p>
<p>https://louiscklaw.github.io/carousell </p>
<p>PM查詢</p>
</IonText>
</div>
</div>
{/* private information */}
<IonList>
<IonItem lines="none">
<h3>Private information</h3>
</IonItem>
<IonItem
lines="full"
button
onClick={(e) => {
route.push('/tabs/carousell_me/OffersMade');
}}
>
<IonText style={{ fontSize: '0.9rem' }}>My Offers</IonText>
</IonItem>
</IonList>
</>
);
};
export default AboutContent;

View File

@@ -0,0 +1,147 @@
import React, { useState } from 'react';
// import { useTranslation } from 'react-i18next';
// import StarRatings from 'react-star-ratings';
import {
IonHeader,
IonTitle,
IonToolbar,
IonContent,
IonPage,
IonButtons,
IonBadge,
IonMenuButton,
IonButton,
IonIcon,
IonDatetime,
IonSelectOption,
IonList,
IonItem,
IonLabel,
IonSelect,
IonPopover,
IonText,
IonCard,
IonCardHeader,
IonCardTitle,
IonCardSubtitle,
IonCardContent,
IonTabs,
IonTabBar,
IonTabButton,
IonSegment,
IonSegmentButton,
useIonRouter,
IonSearchbar,
IonRow,
IonCol,
} from '@ionic/react';
import './style.scss';
import {
locationSharp,
settingsOutline,
qrCode,
shareSocialOutline,
ellipsisHorizontal,
ellipsisVertical,
logoFacebook,
phonePortraitSharp,
pricetagSharp,
archiveSharp,
calendar,
personCircle,
map,
informationCircle,
mailSharp,
chevronDownSharp,
chevronForwardSharp,
filterSharp,
} from 'ionicons/icons';
// import AboutPopover from '../../components/AboutPopover';
import { format, parseISO } from 'date-fns';
// import lookForVisitorSvg from './look_for_visitor.svg';
const SellingButton: React.FC = () => {
return (
<>
<div style={{ width: '50%' }}>
<IonCard style={{ margin: '0.5rem' }} button onClick={(e) => console.log('helloworld')}>
<img
alt="Silhouette of mountains"
src="https://ionicframework.com/docs/img/demos/card-media.png"
/>
<IonCardHeader style={{ padding: '0.5rem' }}>
<IonCardTitle style={{ fontSize: '0.8rem' }}>
{'#html #css #開發 #指導 #代做 #電子平台 #programming #coding #javascript #python #react #debug'
.split('')
.slice(0, 20)}
</IonCardTitle>
<IonCardSubtitle>Card Subtitle</IonCardSubtitle>
</IonCardHeader>
<IonCardContent>View Insights</IonCardContent>
</IonCard>
</div>
</>
);
};
type SessionListProps = {
hide: boolean;
};
const ListingsContent: React.FC<SessionListProps> = ({ hide }) => {
let route = useIonRouter();
let [openExplain, setOpenExplain] = useState(false);
if (hide) return <></>;
return (
<>
<div id="listing-content">
<div style={{ marginTop: '0.5rem', paddingLeft: '0.5rem', paddingRight: '0.5rem' }}>
<IonText style={{ fontWeight: 'bold' }}>33 Listings</IonText>
<IonSearchbar
mode={'ios'}
placeholder="Search the seller's listings"
style={{ padding: 0 }}
></IonSearchbar>
<div style={{ fontSize: '0.8rem' }}>
<IonButton size="small" fill="outline" onClick={(e) => setOpenExplain(true)}>
<IonIcon slot="end" icon={filterSharp} style={{ fontSize: '0.8rem' }}></IonIcon>
Filters
</IonButton>
<IonButton size="small" fill="outline">
<IonText>{`Status: ${'All'}`}</IonText>
</IonButton>
<IonButton size="small" fill="outline">
<IonText>{`In: All Categories`}</IonText>
</IonButton>
</div>
</div>
{/* Sell listing */}
<div
style={{
width: '100%',
display: 'flex',
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-evenly',
}}
>
<SellingButton />
<SellingButton />
<SellingButton />
<SellingButton />
<SellingButton />
<SellingButton />
<SellingButton />
<SellingButton />
</div>
</div>
</>
);
};
export default ListingsContent;

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@@ -0,0 +1,8 @@
#listing-content {
ion-button {
--padding-start: 5px;
--padding-end: 5px;
--padding-top: 5px;
--padding-bottom: 5px;
}
}

View File

@@ -0,0 +1,201 @@
import {
IonBackButton,
IonButton,
IonButtons,
IonCol,
IonContent,
IonGrid,
IonHeader,
IonIcon,
IonInput,
IonItem,
IonLabel,
IonList,
IonMenuButton,
IonPage,
IonPopover,
IonRow,
IonText,
IonTitle,
IonToolbar,
useIonRouter,
useIonViewDidEnter,
} from '@ionic/react';
import { star, starOutline, checkmarkDoneOutline } from 'ionicons/icons';
import React, { useContext, useEffect, useRef, useState } from 'react';
// import AboutPopover from '../../components/AboutPopover';
import { useIonAlert } from '@ionic/react';
import './style.scss';
import { AppContext } from '../../../data/AppContext';
import {
createGesture,
// Gesture
} from '@ionic/react';
import { useTranslation } from 'react-i18next';
// import { ImgPrinterSvg, ImgRiceSvg } from 'src/pages/tabs/carousell_me/settings/svgs';
interface AboutProps {}
const MyProfile: React.FC<AboutProps> = () => {
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 [showAlert, hideAlert] = useIonAlert();
const { browser_store } = useContext(AppContext);
const refRectangle = useRef<HTMLDivElement>(null);
const [swipeType, setSwipeType] = useState();
const [deltaX, setDeltaX] = useState();
const [velocityX, setVelocityX] = useState();
const [swipeVerdict, setSwipeVerdict] = useState('helloworld');
const route = useIonRouter();
const [username, setUsername] = useState('hello user');
const [usernameError, setUsernameError] = useState(false);
useEffect(() => {
if (swipeVerdict == 'swipe-left')
if (route.canGoBack() == true) {
route.goBack();
} else {
route.push('/tabs/schedule');
}
}, [swipeVerdict]);
const onMove = (detail: any) => {
const type = detail.type;
const currentX = detail.currentX;
const deltaX = detail.deltaX;
const velocityX = detail.velocityX;
setSwipeType(type);
setDeltaX(deltaX);
setVelocityX(velocityX);
if (type == 'pan')
if (Math.abs(deltaX) > 50)
if (velocityX > 0) {
setSwipeVerdict('swipe-right');
} else {
setSwipeVerdict('swipe-left');
}
};
useIonViewDidEnter(() => {
let gesture: any = {};
if (refRectangle?.current) {
gesture = createGesture({
gestureName: 'helloworld',
el: refRectangle.current,
onMove: (detail) => {
onMove(detail);
},
});
gesture.enable();
}
return () => {
gesture?.destroy();
};
}, [refRectangle]);
const handleStoreWrite = () => {
(async () => {
await browser_store.set('hello', 'world');
})();
};
const handleStoreRead = () => {
(async () => {
console.log(await browser_store.get('hello'));
})();
};
const handleStoreClear = () => {
(async () => {
await browser_store.clear();
})();
};
const handleStoreRemove = () => {
(async () => {
await browser_store.remove('hello');
})();
};
const handleStoreKeys = () => {
(async () => {
console.dir(await browser_store.keys());
})();
};
const handleStoreLength = () => {
(async () => {
console.dir(await browser_store.length());
})();
};
const userInput = 'javascript:alert("Oh no!")';
const { t } = useTranslation();
return (
<IonPage id="my-profile-page">
<IonHeader translucent={true}>
<IonToolbar>
<IonButtons slot="start">
<IonBackButton defaultHref="/tabs/schedule"></IonBackButton>
</IonButtons>
<IonButtons slot="end">
<IonButton>
<IonIcon slot="icon-only" icon={checkmarkDoneOutline}></IonIcon>
</IonButton>
</IonButtons>
<IonTitle>{t('My Profile')}</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent fullscreen={true}>
<IonHeader collapse="condense">
<IonToolbar>
<IonTitle size="large">{t('My Profile')}</IonTitle>
</IonToolbar>
</IonHeader>
<IonGrid fixed>
<IonList>
<IonItem lines="none">
<IonLabel style={{ fontWeight: 'bold' }}>Public Profile</IonLabel>
</IonItem>
<IonItem lines="none">
<div
style={{
width: '100%',
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
gap: '1rem',
}}
>
<div>Username</div>
<div>
<IonInput placeholder="username"></IonInput>
</div>
</div>
</IonItem>
<IonItem lines="full">
<IonLabel>Public Profile</IonLabel>
</IonItem>
</IonList>
</IonGrid>
</IonContent>
</IonPage>
);
};
export default React.memo(MyProfile);

View File

@@ -0,0 +1,2 @@
#sample-blank-page {
}

View File

@@ -0,0 +1,159 @@
import {
IonBackButton,
IonButton,
IonButtons,
IonContent,
IonHeader,
IonIcon,
IonPage,
IonPopover,
IonTitle,
IonToolbar,
useIonRouter,
useIonViewDidEnter,
} from '@ionic/react';
import { star, starOutline } from 'ionicons/icons';
import React, { useContext, useEffect, useRef, useState } from 'react';
// import AboutPopover from '../../components/AboutPopover';
import { useIonAlert } from '@ionic/react';
import './style.scss';
import { AppContext } from '../../../data/AppContext';
import { createGesture, Gesture } from '@ionic/react';
interface AboutProps {}
const MyProfile: React.FC<AboutProps> = () => {
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 [showAlert, hideAlert] = useIonAlert();
const { browser_store } = useContext(AppContext);
const refRectangle = useRef<HTMLDivElement>(null);
const [swipeType, setSwipeType] = useState();
const [deltaX, setDeltaX] = useState();
const [velocityX, setVelocityX] = useState();
const [swipeVerdict, setSwipeVerdict] = useState('helloworld');
const route = useIonRouter();
useEffect(() => {
if (swipeVerdict == 'swipe-left')
if (route.canGoBack() == true) {
route.goBack();
} else {
route.push('/tabs/schedule');
}
}, [swipeVerdict]);
const onMove = (detail: any) => {
const type = detail.type;
const currentX = detail.currentX;
const deltaX = detail.deltaX;
const velocityX = detail.velocityX;
setSwipeType(type);
setDeltaX(deltaX);
setVelocityX(velocityX);
if (type == 'pan')
if (Math.abs(deltaX) > 50)
if (velocityX > 0) {
setSwipeVerdict('swipe-right');
} else {
setSwipeVerdict('swipe-left');
}
};
useIonViewDidEnter(() => {
let gesture: any = {};
if (refRectangle?.current) {
gesture = createGesture({
gestureName: 'helloworld',
el: refRectangle.current,
onMove: (detail) => {
onMove(detail);
},
});
gesture.enable();
}
return () => {
gesture?.destroy();
};
}, [refRectangle]);
const handleStoreWrite = () => {
(async () => {
await browser_store.set('hello', 'world');
})();
};
const handleStoreRead = () => {
(async () => {
console.log(await browser_store.get('hello'));
})();
};
const handleStoreClear = () => {
(async () => {
await browser_store.clear();
})();
};
const handleStoreRemove = () => {
(async () => {
await browser_store.remove('hello');
})();
};
const handleStoreKeys = () => {
(async () => {
console.dir(await browser_store.keys());
})();
};
const handleStoreLength = () => {
(async () => {
console.dir(await browser_store.length());
})();
};
const userInput = 'javascript:alert("Oh no!")';
return (
<IonPage id="sample-blank-page">
<IonHeader translucent={true}>
<IonToolbar>
<IonButtons slot="start">
<IonBackButton defaultHref="/tabs/schedule"></IonBackButton>
</IonButtons>
<IonTitle>
<h3 style={{ fontWeight: 'bold', fontSize: '0.9rem' }}>Offers Made</h3>
</IonTitle>
<IonButtons slot="end">
<IonButton onClick={() => {}}>
{false ? (
<IonIcon slot="icon-only" icon={star}></IonIcon>
) : (
<IonIcon slot="icon-only" icon={starOutline}></IonIcon>
)}
</IonButton>
</IonButtons>
</IonToolbar>
</IonHeader>
<IonContent>My Profile</IonContent>
{/*
<IonPopover isOpen={showPopover} event={popoverEvent} onDidDismiss={() => setShowPopover(false)}>
<AboutPopover dismiss={() => setShowPopover(false)} />
</IonPopover>
*/}
</IonPage>
);
};
export default React.memo(MyProfile);

View File

@@ -0,0 +1,2 @@
#sample-blank-page {
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.8 KiB

View File

@@ -0,0 +1,207 @@
import React, { useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import StarRatings from 'react-star-ratings';
import {
IonHeader,
IonTitle,
IonToolbar,
IonContent,
IonPage,
IonButtons,
IonBadge,
IonMenuButton,
IonButton,
IonIcon,
IonDatetime,
IonSelectOption,
IonList,
IonItem,
IonLabel,
IonSelect,
IonPopover,
IonText,
IonCard,
IonCardHeader,
IonCardTitle,
IonCardSubtitle,
IonCardContent,
IonTabs,
IonTabBar,
IonTabButton,
IonSegment,
IonSegmentButton,
useIonRouter,
IonAvatar,
IonModal,
} from '@ionic/react';
import './style.scss';
import {
locationSharp,
settingsOutline,
qrCode,
shareSocialOutline,
ellipsisHorizontal,
ellipsisVertical,
logoFacebook,
phonePortraitSharp,
pricetagSharp,
archiveSharp,
calendar,
personCircle,
map,
informationCircle,
mailSharp,
chevronDownSharp,
chevronForwardSharp,
} from 'ionicons/icons';
// import AboutPopover from '../../components/AboutPopover';
import { format, parseISO } from 'date-fns';
import './style.scss';
import AmazingChatSvg from './AmazingChat.svg';
import lookForVisitorSvg from './look_for_visitor.svg';
type SessionListProps = {
hide: boolean;
};
const SampleReview: React.FC = () => {
const modal = useRef<HTMLIonModalElement>(null);
function dismiss() {
modal.current?.dismiss();
}
let [openExplain, setOpenExplain] = useState(false);
return (
<>
<IonModal id="explain-review" ref={modal} isOpen={openExplain}>
<div
className="wrapper"
style={{
padding: '1rem',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
gap: '0.5rem',
}}
>
<h1>Dialog header</h1>
<img src={AmazingChatSvg} width="33%" />
<div>
<IonText style={{ fontSize: '1.2rem', fontWeight: 'bold' }}>Amazing chat</IonText>
</div>
<div style={{ fontSize: '0.9rem' }}>
<p>You enjoyed chatting with this seller,</p>
<p>it felt like you were talking to and old friend</p>
</div>
<IonButton size="default" expand="block" onClick={dismiss}>
Got it!
</IonButton>
</div>
</IonModal>
<div id="sample-review">
<div style={{ padding: '1rem', display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
<div
style={{
display: 'flex',
flexDirection: 'row',
gap: '1rem',
justifyContent: 'center',
alignItems: 'center',
}}
>
<IonAvatar style={{ height: '50px', width: '50px' }}>
<img
alt="Silhouette of a person's head"
src="https://ionicframework.com/docs/img/demos/avatar.svg"
/>
</IonAvatar>
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
<IonText>Man1130</IonText>
<IonText style={{ fontSize: '0.8rem' }} color="dark">
<StarRatings
rating={3}
starRatedColor="green"
changeRating={() => {}}
numberOfStars={5}
name="rating"
starDimension="1rem"
starSpacing="0px"
//
/>
review from buyer 2 months ago
</IonText>
</div>
</div>
<div style={{ fontSize: '0.8rem' }}>
<IonButton size="small" fill="outline" onClick={(e) => setOpenExplain(true)}>
<IonIcon slot="start" icon={chevronForwardSharp}></IonIcon>
Amazing
</IonButton>
<IonButton size="small" fill="outline">
<IonIcon slot="start" icon={chevronForwardSharp}></IonIcon>
Knows
</IonButton>
<IonButton size="small" fill="outline">
<IonIcon slot="start" icon={chevronForwardSharp}></IonIcon>
extra mile
</IonButton>
</div>
<IonText>program之外仲幫左我好多 </IonText>
<div
style={{
padding: '0.5rem',
borderRadius: '0.5rem',
backgroundColor: 'rgb(240, 241, 241)',
display: 'flex',
flexDirection: 'row',
justifyContent: 'flex-start',
gap: '0.5rem',
}}
>
<img
src="https://upload.wikimedia.org/wikipedia/commons/6/6a/JavaScript-logo.png"
style={{ width: '60px', height: '60px' }}
/>
<div style={{ fontSize: '0.8rem' }}>
#html #css # # # # #programming #coding #javascript #python #react
#debug HK$100
</div>
</div>
</div>
</div>
</>
);
};
const ReviewsContent: React.FC<SessionListProps> = ({ hide }) => {
let route = useIonRouter();
if (hide) return <></>;
return (
<>
<div id="review-list">
<SampleReview />
<SampleReview />
<SampleReview />
<SampleReview />
<SampleReview />
<SampleReview />
</div>
</>
);
};
export default ReviewsContent;

View File

@@ -0,0 +1,26 @@
ion-modal#explain-review {
--width: 75vw;
--min-width: 250px;
--height: fit-content;
--border-radius: 6px;
--box-shadow: 0 28px 48px rgba(0, 0, 0, 0.4);
}
ion-modal#explain-review h1 {
margin: 20px 20px 10px 20px;
}
ion-modal#explain-review ion-icon {
margin-right: 6px;
width: 48px;
height: 48px;
padding: 4px 0;
color: #aaaaaa;
}
ion-modal#explain-review .wrapper {
margin-bottom: 10px;
}

View File

@@ -0,0 +1,358 @@
import React, { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import StarRatings from 'react-star-ratings';
import * as selectors from '../../data/selectors';
import {
IonHeader,
IonTitle,
IonToolbar,
IonContent,
IonPage,
IonButtons,
IonBadge,
IonMenuButton,
IonButton,
IonIcon,
IonDatetime,
IonSelectOption,
IonList,
IonItem,
IonLabel,
IonSelect,
IonPopover,
IonText,
IonCard,
IonCardHeader,
IonCardTitle,
IonCardSubtitle,
IonCardContent,
IonTabs,
IonTabBar,
IonTabButton,
IonSegment,
IonSegmentButton,
useIonRouter,
} from '@ionic/react';
import './style.scss';
import {
locationSharp,
settingsOutline,
qrCode,
shareSocialOutline,
ellipsisHorizontal,
ellipsisVertical,
logoFacebook,
phonePortraitSharp,
pricetagSharp,
archiveSharp,
calendar,
personCircle,
map,
informationCircle,
mailSharp,
chevronDownSharp,
chevronForwardSharp,
heartSharp,
heartOutline,
chatbubblesOutline,
} from 'ionicons/icons';
import AboutPopover from '../../components/AboutPopover';
import { format, parseISO } from 'date-fns';
import lookForVisitorSvg from './look_for_visitor.svg';
import AboutContent from './AboutContent';
import ReviewsContent from './ReviewsContent';
import ListingsContent from './ListingsContent';
import { triggerShare } from '../../util/triggerShare';
import { connect } from '../../data/connect';
// import { toProperCase } from 'src/util/toProperCase';
interface OwnProps {}
interface StateProps {
events: Event[];
}
interface DispatchProps {}
interface CarousellMeProps extends OwnProps, StateProps, DispatchProps {}
const CarousellMe: React.FC<CarousellMeProps> = () => {
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 { t } = useTranslation();
const route = useIonRouter();
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 changeRating = () => {
console.log('change rating');
};
// listing , reviews, about
const [segment, setSegment] = useState<'listings' | 'reviews' | 'about'>('listings');
const handleSettingPageClick = () => {
route.push('/tabs/carousell_me/settings');
};
const [isCopied, setIsCopied] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const handleShareLink = useCallback(() => {
triggerShare(`https://louiscklaw.github.io`, 'louis portfolio')
.then(() => {
if (navigator.clipboard) setIsCopied(true);
console.debug('share:copied');
})
.finally(() => {
console.log('share:end');
// setIsOpen(false);
});
}, []);
return (
<IonPage id="carousell-me-page" style={{}}>
<IonContent style={{ paddingBottom: '5rem' }}>
<IonHeader className="ion-no-border">
<IonToolbar>
<IonButtons slot="end">
<IonButton onClick={presentPopover}>
<IonIcon slot="icon-only" icon={heartOutline}></IonIcon>
</IonButton>
<IonButton onClick={presentPopover}>
<IonIcon slot="icon-only" icon={chatbubblesOutline}></IonIcon>
<IonBadge style={{ backgroundColor: 'white', color: 'black' }}>1</IonBadge>
</IonButton>
</IonButtons>
</IonToolbar>
</IonHeader>
<div className="about-header">
{/* Instead of loading an image each time the select changes, use opacity to transition them */}
<div
className="about-image madison"
style={{
//
opacity: location === 'madison' ? '1' : undefined,
}}
></div>
</div>
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: '1.5rem',
paddingLeft: '1rem',
paddingRight: '1rem',
}}
>
<div className="profile-test">
<img
src="https://avatars.githubusercontent.com/u/1376093"
style={{
height: '75px',
width: '75px',
borderRadius: '50px',
border: '3px solid white',
}}
/>
<div className="setting-icons">
<IonButton fill="clear" onClick={handleShareLink}>
<IonIcon
color="primary"
slot="icon-only"
ios={shareSocialOutline}
md={shareSocialOutline}
></IonIcon>
</IonButton>
<IonButton fill="clear" routerLink="/tabs/carousell_me/qr_page">
<IonIcon color="primary" slot="icon-only" ios={qrCode} md={qrCode}></IonIcon>
</IonButton>
<IonButton
fill="clear"
routerLink="/tabs/carousell_me/settings"
// onClick={handleSettingPageClick}
>
<IonIcon
color="primary"
slot="icon-only"
ios={settingsOutline}
md={settingsOutline}
></IonIcon>
</IonButton>
</div>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
<div style={{ fontWeight: 'bold' }}>Louis Law</div>
<div
style={{
fontSize: '0.9rem',
display: 'flex',
flexDirection: 'column',
gap: '0.8rem',
}}
>
<IonText color="medium">@louiscklaw</IonText>
<div
style={{
display: 'flex',
flexDirection: 'row',
alignItems: 'base-line',
gap: '0.2rem',
}}
>
<IonText color="medium">5.0</IonText>
<StarRatings
rating={3}
starRatedColor="green"
changeRating={changeRating}
numberOfStars={5}
name="rating"
starDimension="1rem"
starSpacing="0px"
//
/>
<IonText color="medium">(12)</IonText>
<IonText color="medium">Joined 5y 6m</IonText>
</div>
{/* location */}
<div
style={{
display: 'flex',
flexDirection: 'row',
justifyContent: 'flex-start',
gap: '1rem',
}}
>
<div
style={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
gap: '0.2rem',
}}
>
<IonText color="medium">Verified</IonText>
<IonIcon color="primary" ios={logoFacebook} md={logoFacebook}></IonIcon>
<IonIcon
color="primary"
ios={phonePortraitSharp}
md={phonePortraitSharp}
></IonIcon>
<IonIcon color="primary" ios={mailSharp} md={mailSharp}></IonIcon>
</div>
<div>
<IonText color="medium">
<IonIcon ios={locationSharp} md={locationSharp}></IonIcon>
Hong Kong
</IonText>
</div>
</div>
{/* location */}
{/* conbutton and caroubiz */}
<div>
<IonButton fill="outline" color="primary" shape="round" size="small">
<IonIcon color="primary" ios={pricetagSharp} md={pricetagSharp}></IonIcon>
409
</IonButton>
<IonButton fill="outline" color="primary" shape="round" size="small">
<IonIcon color="primary" ios={archiveSharp} md={archiveSharp}></IonIcon>
caroubiz
</IonButton>
</div>
{/* conbutton and caroubiz */}
{/* profile visitor */}
<IonCard
button
onClick={(e) => route.push('/tabs/carousell_me/insights')}
style={{ margin: '0rem' }}
>
<IonCardContent>
<div
style={{
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
}}
>
<img src={lookForVisitorSvg} height="20%" width="20%" />
<div style={{ marginLeft: '1rem' }}>
<IonText title="helloworld">
<h3 style={{ color: 'black', fontWeight: 'bold' }}>
No profile visitors today
</h3>
</IonText>
<IonText>
<h6 style={{ fontSize: '0.7rem' }}>List an item to get more visitors</h6>
</IonText>
</div>
</div>
</IonCardContent>
</IonCard>
{/* profile visitor */}
</div>
</div>
</div>
{/* listing / reviews / about */}
<IonSegment
mode={'md'}
value={segment}
onIonChange={(e) => setSegment(e.detail.value as any)}
>
<IonSegmentButton value="listings">{t('Listings')}</IonSegmentButton>
<IonSegmentButton value="reviews">{t('Reviwes')}</IonSegmentButton>
<IonSegmentButton value="about">{t('About')}</IonSegmentButton>
</IonSegment>
{/* listing / reviews / about */}
<ListingsContent hide={segment != 'listings'} />
<ReviewsContent hide={segment != 'reviews'} />
<AboutContent hide={segment != 'about'} />
</IonContent>
{/*
<IonPopover isOpen={showPopover} event={popoverEvent} onDidDismiss={() => setShowPopover(false)}>
<AboutPopover dismiss={() => setShowPopover(false)} />
</IonPopover>
*/}
</IonPage>
);
};
// export default React.memo(CarousellMe);
export default connect<OwnProps, StateProps, DispatchProps>({
mapStateToProps: (state) => ({
events: selectors.getEvents(state),
}),
component: React.memo(CarousellMe),
});

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@@ -0,0 +1,118 @@
#carousell-me-page {
.setting-icons {
ion-button::part(native) {
// https://ionicframework.com/docs/theming/css-shadow-parts
margin: 0;
padding: 0;
}
}
ion-content {
--padding-bottom: 5rem;
}
ion-toolbar {
position: absolute;
top: 0;
left: 0;
right: 0;
--background: transparent;
--color: white;
}
ion-toolbar ion-back-button,
ion-toolbar ion-button,
ion-toolbar ion-menu-button {
--color: white;
}
.about-header {
position: relative;
width: 100%;
height: 15%;
}
.about-header .about-image {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
background-position: center;
background-size: cover;
background-repeat: no-repeat;
opacity: 0;
transition: opacity 500ms ease-in-out;
}
.about-header .madison {
background-image: url('https://cdn.dribbble.com/users/438226/screenshots/16695232/media/145ec731468579d04291d11c4c389c65.png');
filter: brightness(75%);
}
.about-info {
position: absolute;
margin-top: -10px;
border-radius: 10px;
background: var(--ion-background-color, #fff);
}
.about-info h3 {
margin-top: 0;
}
.about-info ion-list {
padding-top: 0;
}
.about-info p {
line-height: 130%;
color: var(--ion-color-dark);
}
.about-info ion-icon {
margin-inline-end: 32px;
}
/*
* iOS Only
*/
.ios .about-info {
--ion-padding: 19px;
}
.ios .about-info h3 {
font-weight: 700;
}
.profile-test {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: flex-end;
position: relative;
height: 50px;
ion-icon {
font-size: 1.5rem;
margin-left: 1rem;
}
}
}
#date-input-popover {
--offset-y: -var(--ion-safe-area-bottom);
--max-width: 90%;
--width: 336px;
}

View File

@@ -19,6 +19,7 @@ import PATHS from '../PATHS';
import Favourites from './Favourites';
import Helloworld from './Helloworld';
import TabAppRoute from '../TabAppRoute';
import CarousellMe from './CarousellMe';
interface MainTabsProps {}
@@ -43,6 +44,16 @@ const MainTabs: React.FC<MainTabsProps> = () => {
<Route path="/tabs/about" render={() => <About />} exact={true} />
<Route path="/tabs/helloworld" render={() => <Helloworld />} exact={true} />
{/* */}
<Route path={PATHS.CAROUSELL_ME} render={() => <CarousellMe />} exact={true} />
{/*
<Route path="/tabs/carousell_me/insights" component={Insights} exact={true} />
<Route path="/tabs/carousell_me/OffersMade" component={OffersMade} exact={true} />
<Route path="/tabs/carousell_me/settings" component={Settings} exact={true} />
<Route path="/tabs/carousell_me/my_profile" component={MyProfile} exact={true} />
<Route path="/tabs/carousell_me/qr_page" component={QRPage} exact={true} />
*/}
{/* */}
<TabAppRoute />
</IonRouterOutlet>
@@ -63,10 +74,14 @@ const MainTabs: React.FC<MainTabsProps> = () => {
<IonIcon icon={people} />
<IonLabel>Nearby</IonLabel>
</IonTabButton>
{/*
<IonTabButton tab="orders" href={PATHS.ORDERS_LIST}>
<IonIcon icon={location} />
<IonLabel>Order</IonLabel>
</IonTabButton>
*/}
<IonTabButton disabled></IonTabButton>
<IonTabButton tab="message" href={PATHS.MESSAGE_LIST}>
<IonIcon icon={informationCircle} />
<IonLabel>Message</IonLabel>

View File

@@ -0,0 +1,5 @@
export const toProperCase = (txt: string) => {
return txt.replace(/\w\S*/g, function (txt) {
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
});
};

View File

@@ -0,0 +1,8 @@
export const triggerShare = (url: string, title: string) => {
if (navigator.share) {
return navigator?.share({ title, url });
} else if (navigator.clipboard) {
return navigator.clipboard.writeText(url);
}
return new Promise((resolve) => resolve(''));
};

View File

@@ -0,0 +1 @@
export const hello = 'world';