update,
@@ -13,6 +13,7 @@ dependencies {
|
||||
implementation project(':capacitor-clipboard')
|
||||
implementation project(':capacitor-geolocation')
|
||||
implementation project(':capacitor-preferences')
|
||||
implementation project(':capacitor-share')
|
||||
|
||||
}
|
||||
|
||||
|
@@ -13,3 +13,6 @@ project(':capacitor-geolocation').projectDir = new File('../node_modules/@capaci
|
||||
|
||||
include ':capacitor-preferences'
|
||||
project(':capacitor-preferences').projectDir = new File('../node_modules/@capacitor/preferences/android')
|
||||
|
||||
include ':capacitor-share'
|
||||
project(':capacitor-share').projectDir = new File('../node_modules/@capacitor/share/android')
|
||||
|
@@ -15,6 +15,7 @@ def capacitor_pods
|
||||
pod 'CapacitorClipboard', :path => '../../node_modules/@capacitor/clipboard'
|
||||
pod 'CapacitorGeolocation', :path => '../../node_modules/@capacitor/geolocation'
|
||||
pod 'CapacitorPreferences', :path => '../../node_modules/@capacitor/preferences'
|
||||
pod 'CapacitorShare', :path => '../../node_modules/@capacitor/share'
|
||||
end
|
||||
|
||||
target 'App' do
|
||||
|
@@ -13,6 +13,7 @@
|
||||
"@capacitor/geolocation": "^7.1.2",
|
||||
"@capacitor/ios": "7.0.1",
|
||||
"@capacitor/preferences": "^7.0.0",
|
||||
"@capacitor/share": "^7.0.1",
|
||||
"@hookform/resolvers": "^4.1.3",
|
||||
"@ionic/react": "^8.5.0",
|
||||
"@ionic/react-router": "^8.5.0",
|
||||
@@ -27,6 +28,7 @@
|
||||
"pigeon-maps": "^0.22.1",
|
||||
"pullstate": "^1",
|
||||
"react": "19.0.0",
|
||||
"react-color": "^2.19.3",
|
||||
"react-confetti": "^6.4.0",
|
||||
"react-dom": "19.0.0",
|
||||
"react-hook-form": "^7.55.0",
|
||||
|
BIN
03_source/mobile/public/assets/DemoQuizApp/icon/favicon.png
Normal file
After Width: | Height: | Size: 930 B |
BIN
03_source/mobile/public/assets/DemoQuizApp/icon/icon.png
Normal file
After Width: | Height: | Size: 23 KiB |
BIN
03_source/mobile/public/assets/DemoQuizApp/main.png
Normal file
After Width: | Height: | Size: 39 KiB |
1
03_source/mobile/public/assets/DemoQuizApp/shapes.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="350" height="140" xmlns="http://www.w3.org/2000/svg" style="background:#f6f7f9"><g fill="none" fill-rule="evenodd"><path fill="#F04141" style="mix-blend-mode:multiply" d="M61.905-34.23l96.194 54.51-66.982 54.512L22 34.887z"/><circle fill="#10DC60" style="mix-blend-mode:multiply" cx="155.5" cy="135.5" r="57.5"/><path fill="#3880FF" style="mix-blend-mode:multiply" d="M208.538 9.513l84.417 15.392L223.93 93.93z"/><path fill="#FFCE00" style="mix-blend-mode:multiply" d="M268.625 106.557l46.332-26.75 46.332 26.75v53.5l-46.332 26.75-46.332-26.75z"/><circle fill="#7044FF" style="mix-blend-mode:multiply" cx="299.5" cy="9.5" r="38.5"/><rect fill="#11D3EA" style="mix-blend-mode:multiply" transform="rotate(-60 148.47 37.886)" x="143.372" y="-7.056" width="10.196" height="89.884" rx="5.098"/><path d="M-25.389 74.253l84.86 8.107c5.498.525 9.53 5.407 9.004 10.905a10 10 0 0 1-.057.477l-12.36 85.671a10.002 10.002 0 0 1-11.634 8.42l-86.351-15.226c-5.44-.959-9.07-6.145-8.112-11.584l13.851-78.551a10 10 0 0 1 10.799-8.219z" fill="#7044FF" style="mix-blend-mode:multiply"/><circle fill="#0CD1E8" style="mix-blend-mode:multiply" cx="273.5" cy="106.5" r="20.5"/></g></svg>
|
After Width: | Height: | Size: 1.1 KiB |
BIN
03_source/mobile/public/assets/DemoSlidingProfile/autumn.png
Normal file
After Width: | Height: | Size: 235 KiB |
BIN
03_source/mobile/public/assets/DemoSlidingProfile/avatar.jpeg
Normal file
After Width: | Height: | Size: 52 KiB |
BIN
03_source/mobile/public/assets/DemoSlidingProfile/avatar1.png
Normal file
After Width: | Height: | Size: 358 KiB |
BIN
03_source/mobile/public/assets/DemoSlidingProfile/avatar2.png
Normal file
After Width: | Height: | Size: 424 KiB |
BIN
03_source/mobile/public/assets/DemoSlidingProfile/avatar3.png
Normal file
After Width: | Height: | Size: 56 KiB |
BIN
03_source/mobile/public/assets/DemoSlidingProfile/avatar4.png
Normal file
After Width: | Height: | Size: 133 KiB |
BIN
03_source/mobile/public/assets/DemoSlidingProfile/avatar5.png
Normal file
After Width: | Height: | Size: 264 KiB |
BIN
03_source/mobile/public/assets/DemoSlidingProfile/avatar6.png
Normal file
After Width: | Height: | Size: 67 KiB |
BIN
03_source/mobile/public/assets/DemoSlidingProfile/cover1.jpeg
Normal file
After Width: | Height: | Size: 249 KiB |
BIN
03_source/mobile/public/assets/DemoSlidingProfile/cover2.jpeg
Normal file
After Width: | Height: | Size: 216 KiB |
BIN
03_source/mobile/public/assets/DemoSlidingProfile/cover4.jpeg
Normal file
After Width: | Height: | Size: 180 KiB |
BIN
03_source/mobile/public/assets/DemoSlidingProfile/cover5.jpeg
Normal file
After Width: | Height: | Size: 110 KiB |
BIN
03_source/mobile/public/assets/DemoSlidingProfile/cover6.jpeg
Normal file
After Width: | Height: | Size: 124 KiB |
BIN
03_source/mobile/public/assets/DemoSlidingProfile/flower.jpeg
Normal file
After Width: | Height: | Size: 78 KiB |
BIN
03_source/mobile/public/assets/DemoSlidingProfile/h.jpeg
Normal file
After Width: | Height: | Size: 92 KiB |
After Width: | Height: | Size: 930 B |
BIN
03_source/mobile/public/assets/DemoSlidingProfile/icon/icon.png
Normal file
After Width: | Height: | Size: 23 KiB |
BIN
03_source/mobile/public/assets/DemoSlidingProfile/ocean.jpeg
Normal file
After Width: | Height: | Size: 64 KiB |
@@ -0,0 +1 @@
|
||||
<svg width="350" height="140" xmlns="http://www.w3.org/2000/svg" style="background:#f6f7f9"><g fill="none" fill-rule="evenodd"><path fill="#F04141" style="mix-blend-mode:multiply" d="M61.905-34.23l96.194 54.51-66.982 54.512L22 34.887z"/><circle fill="#10DC60" style="mix-blend-mode:multiply" cx="155.5" cy="135.5" r="57.5"/><path fill="#3880FF" style="mix-blend-mode:multiply" d="M208.538 9.513l84.417 15.392L223.93 93.93z"/><path fill="#FFCE00" style="mix-blend-mode:multiply" d="M268.625 106.557l46.332-26.75 46.332 26.75v53.5l-46.332 26.75-46.332-26.75z"/><circle fill="#7044FF" style="mix-blend-mode:multiply" cx="299.5" cy="9.5" r="38.5"/><rect fill="#11D3EA" style="mix-blend-mode:multiply" transform="rotate(-60 148.47 37.886)" x="143.372" y="-7.056" width="10.196" height="89.884" rx="5.098"/><path d="M-25.389 74.253l84.86 8.107c5.498.525 9.53 5.407 9.004 10.905a10 10 0 0 1-.057.477l-12.36 85.671a10.002 10.002 0 0 1-11.634 8.42l-86.351-15.226c-5.44-.959-9.07-6.145-8.112-11.584l13.851-78.551a10 10 0 0 1 10.799-8.219z" fill="#7044FF" style="mix-blend-mode:multiply"/><circle fill="#0CD1E8" style="mix-blend-mode:multiply" cx="273.5" cy="106.5" r="20.5"/></g></svg>
|
After Width: | Height: | Size: 1.1 KiB |
BIN
03_source/mobile/public/assets/DemoSlidingProfile/spring.png
Normal file
After Width: | Height: | Size: 288 KiB |
BIN
03_source/mobile/public/assets/DemoSlidingProfile/summer.png
Normal file
After Width: | Height: | Size: 210 KiB |
BIN
03_source/mobile/public/assets/DemoSlidingProfile/van.jpeg
Normal file
After Width: | Height: | Size: 174 KiB |
BIN
03_source/mobile/public/assets/DemoSlidingProfile/winter.png
Normal file
After Width: | Height: | Size: 206 KiB |
@@ -78,6 +78,20 @@ import DemoDictionaryApp from './pages/DemoDictionaryApp';
|
||||
// demo-recipe-app
|
||||
import DemoRecipeApp from './pages/DemoRecipeApp';
|
||||
|
||||
// DemoSlidingProfile
|
||||
import DemoSlidingProfile from './pages/DemoSlidingProfile';
|
||||
|
||||
// DemoQuizApp
|
||||
import DemoQuizApp from './pages/DemoQuizApp';
|
||||
import DemoBlogPostUi from './pages/DemoBlogPostUi';
|
||||
import DemoReactTravelApp from './pages/DemoReactTravelApp';
|
||||
import DemoPinterestFloatingTabBar from './pages/DemoPinterestFloatingTabBar';
|
||||
import DemoRestaurantFinder from './pages/DemoRestaurantFinder';
|
||||
import DemoReactOverlayHooks from './pages/DemoReactOverlayHooks';
|
||||
import DemoReactSwitchTabs from './pages/DemoReactSwitchTabs';
|
||||
import DemoReactPollApp from './pages/DemoReactPollApp';
|
||||
import DemoReactWhatsAppClone from './pages/DemoReactWhatsAppClone';
|
||||
|
||||
setupIonicReact();
|
||||
|
||||
const App: React.FC = () => {
|
||||
@@ -102,14 +116,7 @@ interface DispatchProps {
|
||||
|
||||
interface IonicAppProps extends StateProps, DispatchProps {}
|
||||
|
||||
const IonicApp: React.FC<IonicAppProps> = ({
|
||||
darkMode,
|
||||
schedule,
|
||||
setIsLoggedIn,
|
||||
setUsername,
|
||||
loadConfData,
|
||||
loadUserData,
|
||||
}) => {
|
||||
const IonicApp: React.FC<IonicAppProps> = ({ darkMode, schedule, setIsLoggedIn, setUsername, loadConfData, loadUserData }) => {
|
||||
useEffect(() => {
|
||||
loadUserData();
|
||||
loadConfData();
|
||||
@@ -132,19 +139,34 @@ const IonicApp: React.FC<IonicAppProps> = ({
|
||||
|
||||
<AppRoute />
|
||||
|
||||
{/* */}
|
||||
<Route path="/tabs" render={() => <MainTabs />} />
|
||||
<Route path={paths.DEMO_REACT_SHOP} render={() => <DemoReactShop />} />
|
||||
<Route path={paths.DEMO_WEATHER_APP} render={() => <DemoWeatherApp />} />
|
||||
<Route path={paths.DEMO_CLUB_HOUSE} render={() => <DemoClubHouse />} />
|
||||
<Route path={paths.DEMO_SCORE_BOARD} render={() => <DemoScoreBoard />} />
|
||||
<Route path={paths.DEMO_QUOTE_APP} render={() => <DemoQuoteApp />} />
|
||||
<Route path={paths.DEMO_QR_SCANNER} render={() => <DemoQrScanner />} />
|
||||
<Route path={paths.DEMO_SHOP_APP_UI} render={() => <DemoShopAppUi />} />
|
||||
<Route path={paths.DEMO_DICTIONARY_APP} render={() => <DemoDictionaryApp />} />
|
||||
<Route path={paths.DEMO_RECIPE_APP} render={() => <DemoRecipeApp />} />
|
||||
|
||||
{/* */}
|
||||
{/* */}
|
||||
{/* */}
|
||||
{/* */}
|
||||
<Route path={paths.DEMO_REACT_WHATSAPP_CLONE} render={() => <DemoReactWhatsAppClone />} />
|
||||
|
||||
<Route path={paths.DEMO_REACT_POLL_APP} render={() => <DemoReactPollApp />} />
|
||||
|
||||
<Route path={paths.DEMO_BLOG_POST_UI} render={() => <DemoBlogPostUi />} />
|
||||
<Route path={paths.DEMO_CLUB_HOUSE} render={() => <DemoClubHouse />} />
|
||||
<Route path={paths.DEMO_DICTIONARY_APP} render={() => <DemoDictionaryApp />} />
|
||||
<Route path={paths.DEMO_PINTEREST_FLOATING_TAB_BAR} render={() => <DemoPinterestFloatingTabBar />} />
|
||||
<Route path={paths.DEMO_QR_SCANNER} render={() => <DemoQrScanner />} />
|
||||
<Route path={paths.DEMO_QUIZ_APP} render={() => <DemoQuizApp />} />
|
||||
<Route path={paths.DEMO_QUOTE_APP} render={() => <DemoQuoteApp />} />
|
||||
<Route path={paths.DEMO_REACT_OVERLAY_HOOKS} render={() => <DemoReactOverlayHooks />} />
|
||||
<Route path={paths.DEMO_REACT_POLL_APP} render={() => <DemoReactPollApp />} />
|
||||
<Route path={paths.DEMO_REACT_SHOP} render={() => <DemoReactShop />} />
|
||||
<Route path={paths.DEMO_REACT_SWITCH_TABS} render={() => <DemoReactSwitchTabs />} />
|
||||
<Route path={paths.DEMO_REACT_TRAVEL_APP} render={() => <DemoReactTravelApp />} />
|
||||
<Route path={paths.DEMO_RECIPE_APP} render={() => <DemoRecipeApp />} />
|
||||
<Route path={paths.DEMO_RESTAURANT_FINDER} render={() => <DemoRestaurantFinder />} />
|
||||
<Route path={paths.DEMO_SCORE_BOARD} render={() => <DemoScoreBoard />} />
|
||||
<Route path={paths.DEMO_SHOP_APP_UI} render={() => <DemoShopAppUi />} />
|
||||
<Route path={paths.DEMO_SLIDING_PROFILE} render={() => <DemoSlidingProfile />} />
|
||||
|
||||
<Route path="/account" component={Account} />
|
||||
<Route path="/login" component={Login} />
|
||||
<Route path="/mylogin" component={MyLogin} />
|
||||
@@ -153,7 +175,6 @@ const IonicApp: React.FC<IonicAppProps> = ({
|
||||
<Route path="/support" component={Support} />
|
||||
<Route path="/tutorial" component={Tutorial} />
|
||||
|
||||
{/* */}
|
||||
<Route
|
||||
path="/logout"
|
||||
render={() => {
|
||||
|
@@ -0,0 +1,7 @@
|
||||
.view-post-footer {
|
||||
|
||||
background-color: white;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
}
|
@@ -0,0 +1,90 @@
|
||||
import {
|
||||
IonBackButton,
|
||||
IonBadge,
|
||||
IonButton,
|
||||
IonButtons,
|
||||
IonCardSubtitle,
|
||||
IonCardTitle,
|
||||
IonCol,
|
||||
IonContent,
|
||||
IonFooter,
|
||||
IonGrid,
|
||||
IonHeader,
|
||||
IonIcon,
|
||||
IonNote,
|
||||
IonPage,
|
||||
IonRow,
|
||||
IonText,
|
||||
IonTitle,
|
||||
IonToolbar,
|
||||
} from '@ionic/react';
|
||||
import { bookmarkOutline, shareOutline } from 'ionicons/icons';
|
||||
import { useParams } from 'react-router';
|
||||
import { blogPosts } from '../localData';
|
||||
import './BlogPost.css';
|
||||
|
||||
const BlogPost = () => {
|
||||
const { id } = useParams();
|
||||
const post = blogPosts.filter((post) => parseInt(post.id) === parseInt(id))[0];
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonTitle>Blog</IonTitle>
|
||||
<IonButtons slot="start">
|
||||
<IonBackButton text="Blog Posts" />
|
||||
</IonButtons>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<IonContent fullscreen>
|
||||
<img src={post.image} alt="post header" />
|
||||
|
||||
<IonGrid className="ion-padding-start ion-padding-end">
|
||||
<IonRow className="ion-align-items-center ion-justify-content-between">
|
||||
<IonRow className="ion-align-items-center ion-justify-content-between">
|
||||
<img src={post.authorImage} className="post-author-avatar" alt="post author" />
|
||||
<IonCardSubtitle className="ion-no-margin ion-no-padding ion-margin-start">
|
||||
{post.author}
|
||||
</IonCardSubtitle>
|
||||
</IonRow>
|
||||
<IonNote>{post.date}</IonNote>
|
||||
</IonRow>
|
||||
|
||||
<IonRow>
|
||||
<IonCol size="12">
|
||||
<IonCardTitle className="post-title">{post.title}</IonCardTitle>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
|
||||
<IonRow>
|
||||
<IonCol size="12">
|
||||
<IonText color="medium">{post.content}</IonText>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
</IonGrid>
|
||||
</IonContent>
|
||||
|
||||
<IonFooter className="view-post-footer">
|
||||
<IonRow className="post-footer ion-align-self-center ion-justify-content-between">
|
||||
<div>
|
||||
<IonButton fill="clear" color="primary">
|
||||
<IonIcon icon={shareOutline} />
|
||||
</IonButton>
|
||||
<IonButton fill="clear" color="primary">
|
||||
<IonIcon icon={bookmarkOutline} />
|
||||
</IonButton>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<IonBadge color="primary" className="post-category">
|
||||
{post.category}
|
||||
</IonBadge>
|
||||
</div>
|
||||
</IonRow>
|
||||
</IonFooter>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default BlogPost;
|
51
03_source/mobile/src/pages/DemoBlogPostUi/AppPages/Home.jsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import {
|
||||
IonButton,
|
||||
IonButtons,
|
||||
IonContent,
|
||||
IonHeader,
|
||||
IonIcon,
|
||||
IonPage,
|
||||
IonTitle,
|
||||
IonToolbar,
|
||||
useIonRouter,
|
||||
} from '@ionic/react';
|
||||
import { Post } from '../components/Post';
|
||||
import { blogPosts } from '../localData';
|
||||
import { chevronBackOutline } from 'ionicons/icons';
|
||||
|
||||
const Home = () => {
|
||||
const router = useIonRouter();
|
||||
|
||||
function handleBackClick() {
|
||||
router.goBack();
|
||||
}
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonTitle>Ionic Blog</IonTitle>
|
||||
|
||||
<IonButtons slot="start">
|
||||
<IonButton onClick={() => handleBackClick()}>
|
||||
<IonIcon icon={chevronBackOutline} color="primary" />
|
||||
</IonButton>
|
||||
</IonButtons>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<IonContent fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large">Ionic Blog</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
{blogPosts.map((post, index) => (
|
||||
<Post post={post} key={`post_${index}`} />
|
||||
))}
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
@@ -0,0 +1,37 @@
|
||||
.post-author-avatar {
|
||||
height: 2rem;
|
||||
width: 2rem;
|
||||
border-radius: 500px;
|
||||
}
|
||||
|
||||
.post-title {
|
||||
font-size: 1.4rem;
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.post-content {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.post-footer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-content: center;
|
||||
width: 100%;
|
||||
border-top: 2px solid rgb(245, 245, 245);
|
||||
margin-top: 2rem;
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.post-category {
|
||||
margin-top: 1.1rem;
|
||||
}
|
||||
|
||||
.post-image {
|
||||
width: 100%;
|
||||
}
|
@@ -0,0 +1,57 @@
|
||||
import {
|
||||
IonBadge,
|
||||
IonButton,
|
||||
IonCard,
|
||||
IonCardContent,
|
||||
IonCardHeader,
|
||||
IonCardSubtitle,
|
||||
IonCardTitle,
|
||||
IonIcon,
|
||||
IonNote,
|
||||
IonRow,
|
||||
} from '@ionic/react';
|
||||
import { bookmarkOutline, shareOutline } from 'ionicons/icons';
|
||||
|
||||
import './Post.css';
|
||||
|
||||
export const Post = ({ post }) => {
|
||||
return (
|
||||
<IonCard routerLink={`/demo-blog-post-ui/post/${post.id}`}>
|
||||
<img src={post.image} alt="main post" className="post-image" />
|
||||
|
||||
<IonCardHeader>
|
||||
<IonRow className="ion-align-items-center ion-justify-content-between">
|
||||
<IonRow className="ion-align-items-center ion-justify-content-between">
|
||||
<img src={post.authorImage} className="post-author-avatar" alt="post author" />
|
||||
<IonCardSubtitle className="ion-no-margin ion-no-padding ion-margin-start">
|
||||
{post.author}
|
||||
</IonCardSubtitle>
|
||||
</IonRow>
|
||||
<IonNote>{post.date}</IonNote>
|
||||
</IonRow>
|
||||
<IonCardTitle className="post-title">{post.title}</IonCardTitle>
|
||||
</IonCardHeader>
|
||||
|
||||
<IonCardContent>
|
||||
<p className="post-content">{post.content}</p>
|
||||
|
||||
<IonRow className="post-footer ion-align-self-center ion-justify-content-between">
|
||||
<div>
|
||||
<IonButton fill="clear" color="primary">
|
||||
<IonIcon icon={shareOutline} />
|
||||
</IonButton>
|
||||
<IonButton fill="clear" color="primary">
|
||||
<IonIcon icon={bookmarkOutline} />
|
||||
</IonButton>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<IonBadge color="primary" className="post-category">
|
||||
{post.category}
|
||||
</IonBadge>
|
||||
</div>
|
||||
</IonRow>
|
||||
</IonCardContent>
|
||||
</IonCard>
|
||||
);
|
||||
};
|
27
03_source/mobile/src/pages/DemoBlogPostUi/index.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { IonIcon, IonLabel, IonRouterOutlet, IonTabBar, IonTabButton, IonTabs } from '@ionic/react';
|
||||
|
||||
import { cloudOutline, searchOutline } from 'ionicons/icons';
|
||||
import { Route, Redirect } from 'react-router';
|
||||
|
||||
import Home from './AppPages/Home';
|
||||
import BlogPost from './AppPages/BlogPost';
|
||||
|
||||
import './style.scss';
|
||||
|
||||
function DemoBlogPostUi() {
|
||||
return (
|
||||
<IonRouterOutlet className="demo-blog-post-ui">
|
||||
<Route exact path="/demo-blog-post-ui/home">
|
||||
<Home />
|
||||
</Route>
|
||||
|
||||
<Route exact path="/demo-blog-post-ui/post/:id">
|
||||
<BlogPost />
|
||||
</Route>
|
||||
|
||||
<Redirect exact path="/demo-blog-post-ui" to="/demo-blog-post-ui/home" />
|
||||
</IonRouterOutlet>
|
||||
);
|
||||
}
|
||||
|
||||
export default DemoBlogPostUi;
|
98
03_source/mobile/src/pages/DemoBlogPostUi/localData/index.js
Normal file
@@ -0,0 +1,98 @@
|
||||
export const blogPosts = [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "How to Convince Your Boss to Choose Ionic",
|
||||
"title_link": "https://ionicframework.com/blog/convince-boss-choose-ionic-app-development/",
|
||||
"date": "August 3, 2021",
|
||||
"author": "By Kim Maida",
|
||||
"authorImage": "https://ionicframework.com/blog/wp-content/uploads/2021/07/kim-maida-150x150.jpg",
|
||||
"category": "ANNOUNCEMENTS",
|
||||
"category_link": "https://ionicframework.com/blog//blog/category/announcements",
|
||||
"image": "https://ionicframework.com/blog/wp-content/uploads/2021/07/how-to-convince-your-boss_image_1aug2021.png",
|
||||
"content": "Greetings, friend! You’re a web developer, team lead, or engineering manager who has discovered that Ionic products are awesome. They have helped you build cross-platform applications quickly, made the app development process enjoyable, and solved important mobile development problems. You can see that Ionic would be extremely beneficial in your daily job, but are wondering how to convince your boss to endorse the adoption of new software. In a nutshell:"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"title": "Ioniconf 2021 Conference Recap",
|
||||
"title_link": "https://ionicframework.com/blog/ioniconf-2021-conference-recap/",
|
||||
"date": "July 29, 2021",
|
||||
"author": "By Mike Hartington",
|
||||
"authorImage": "https://ionicframework.com/blog/wp-content/uploads/2018/08/mike-headshot-2-smaller-150x150.png",
|
||||
"category": "ANNOUNCEMENTS",
|
||||
"category_link": "https://ionicframework.com/blog//blog/category/announcements",
|
||||
"image": "https://ionicframework.com/blog/wp-content/uploads/2021/06/og-imgx2.png",
|
||||
"content": "And with that, Ioniconf 2021 has concluded! Ioniconf, our online conference for Ionic developers and the wider web development community, featured twelve expert Ionic speakers and was attended by many thousands of Ionic community members. We’re thrilled by the community’s reception to the event and are already looking forward to our next event taking place in September. Read on for a recap and links to all recorded talks."
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"title": "Announcing Identity Vault 5.0",
|
||||
"title_link": "https://ionicframework.com/blog/announcing-identity-vault-5-0/",
|
||||
"date": "July 28, 2021",
|
||||
"author": "By Dallas James",
|
||||
"authorImage": "https://ionicframework.com/blog/wp-content/uploads/2021/07/dallas-james-150x150.jpg",
|
||||
"category": "PRODUCT",
|
||||
"category_link": "https://ionicframework.com/blog//blog/category/announcements",
|
||||
"image": "https://ionicframework.com/blog/wp-content/uploads/2021/07/iv-5-feature-image.png",
|
||||
"content": "Today I’m excited to announce Identity Vault 5.0, the newest version of Ionic’s mobile biometrics solution. Featuring the latest in native security best practices, Identity Vault improves frontend security in any Ionic app by making it easy to add secure biometric authentication in minutes."
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"title": "Building with Stencil: Clock Component",
|
||||
"title_link": "https://ionicframework.com/blog/building-with-stencil-clock-component/",
|
||||
"date": "July 22, 2021",
|
||||
"author": "By Kevin Hoyt",
|
||||
"authorImage": "https://ionicframework.com/blog/wp-content/uploads/2021/07/2520666-150x150.jpg",
|
||||
"category": "ANNOUNCEMENTS",
|
||||
"category_link": "https://ionicframework.com/blog//blog/category/announcements",
|
||||
"image": "https://ionicframework.com/blog/wp-content/uploads/2021/07/Image-from-iOS.png",
|
||||
"content": "I have not seen a clock in a web-based user interface in a long time. This makes sense — they are pretty redundant these days. You have a clock on your watch, on your mobile device, and on your desktop, and those are just the digital versions available at a glance. Nonetheless, the process of building a clock can reveal a lot about how a platform works."
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"title": "Building with Stencil: Calendar Component",
|
||||
"title_link": "https://ionicframework.com/blog/building-with-stencil-calendar-component/",
|
||||
"date": "July 19, 2021",
|
||||
"author": "By Kevin Hoyt",
|
||||
"authorImage": "https://ionicframework.com/blog/wp-content/uploads/2021/07/2520666-150x150.jpg",
|
||||
"category": "TUTORIALS",
|
||||
"category_link": null,
|
||||
"image": "https://ionicframework.com/blog/wp-content/uploads/2021/07/ionic-blog-post-image_first-look-01.png",
|
||||
"content": "Take a look at the month view of a calendar and you will see several rows of numbers. The numbers themselves, increasing in value one after the other, are arranged in columns. HTML and CSS provide us with a number of tools to display content in rows and columns. Making a calendar component should be easy, right? Right?"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"title": "Introducing the New Overlay Hooks for Ionic React",
|
||||
"title_link": "https://ionicframework.com/blog/introducing-the-new-overlay-hooks-for-ionic-react/",
|
||||
"date": "July 14, 2021",
|
||||
"author": "By Ely Lucas",
|
||||
"authorImage": "https://secure.gravatar.com/avatar/45ad19965b4bde97e9f4396ea01ed184?s=32&r=g",
|
||||
"category": "ENGINEERING",
|
||||
"category_link": null,
|
||||
"image": "https://ionicframework.com/blog/wp-content/uploads/2021/07/react-overlay-hooks-feature-image.png",
|
||||
"content": "Hello Friends! We know everyone is excited about the new features in Ionic Framework 6.0 beta, but that doesn’t mean we’re done with V5! In Ionic React 5.6, we packaged up a new set of hooks for controlling our overlay components that we think you might like. What is an overlay you ask? It’s the term we give components that display over your current content, such as alerts, modals, toasts, etc."
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"title": "The Future of Stencil: Expanded Team, New Software Platform, and More",
|
||||
"title_link": "https://ionicframework.com/blog/the-future-of-stencil-expanded-team-new-software-platform-and-more/",
|
||||
"date": "July 7, 2021",
|
||||
"author": "By Nick Hyatt",
|
||||
"authorImage": "https://ionicframework.com/blog/wp-content/uploads/2018/11/Nick-Hyatt-Headshot-150x150.jpeg",
|
||||
"category": "ANNOUNCEMENTS",
|
||||
"category_link": null,
|
||||
"image": "https://ionicframework.com/blog/wp-content/uploads/2021/07/stencil-future-feature-image.png",
|
||||
"content": "Today I’m excited to share some news about Stencil, Ionic’s open source toolchain that generates small, fast, and 100% standards-based Web Components that run in every browser. As you might have noticed, we’ve been actively increasing our investments across the entire Ionic App Platform, including the recent launch of Capacitor 3.0, Ionic Portals, tons of Appflow improvements, and the upcoming Ionic Framework v6."
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"title": "Announcing the Ionic Framework v6 Beta",
|
||||
"title_link": "https://ionicframework.com/blog/announcing-the-ionic-framework-v6-beta/",
|
||||
"date": "June 29, 2021",
|
||||
"author": "By Liam DeBeasi",
|
||||
"authorImage": "https://ionicframework.com/blog/wp-content/uploads/2020/01/ZNK4lRAJ_400x400-150x150.jpg",
|
||||
"category": "ANNOUNCEMENTS",
|
||||
"category_link": null,
|
||||
"image": "https://ionicframework.com/blog/wp-content/uploads/2021/06/framework6-feature-image.png",
|
||||
"content": "Earlier this week I had the privilege of giving the Ionic Framework Update at Ioniconf 2021 where we announced the Ionic Framework v6 beta. Ionic Framework has come far from its roots as an AngularJS-only UI library to a truly cross-platform framework for building Web Native applications. As we look to the future of Ionic Framework, let’s talk about some of the improvements coming in Framework v6 and how you can get access to these improvements today."
|
||||
}
|
||||
];
|
79
03_source/mobile/src/pages/DemoBlogPostUi/style.scss
Normal file
@@ -0,0 +1,79 @@
|
||||
.demo-blog-post-ui {
|
||||
/* Ionic Variables and Theming. For more info, please see:
|
||||
http://ionicframework.com/docs/theming/ */
|
||||
|
||||
/** Ionic CSS Variables **/
|
||||
* {
|
||||
/** primary **/
|
||||
--ion-color-primary: #3880ff;
|
||||
--ion-color-primary-rgb: 56, 128, 255;
|
||||
--ion-color-primary-contrast: #ffffff;
|
||||
--ion-color-primary-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-primary-shade: #3171e0;
|
||||
--ion-color-primary-tint: #4c8dff;
|
||||
|
||||
/** secondary **/
|
||||
--ion-color-secondary: #3dc2ff;
|
||||
--ion-color-secondary-rgb: 61, 194, 255;
|
||||
--ion-color-secondary-contrast: #ffffff;
|
||||
--ion-color-secondary-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-secondary-shade: #36abe0;
|
||||
--ion-color-secondary-tint: #50c8ff;
|
||||
|
||||
/** tertiary **/
|
||||
--ion-color-tertiary: #5260ff;
|
||||
--ion-color-tertiary-rgb: 82, 96, 255;
|
||||
--ion-color-tertiary-contrast: #ffffff;
|
||||
--ion-color-tertiary-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-tertiary-shade: #4854e0;
|
||||
--ion-color-tertiary-tint: #6370ff;
|
||||
|
||||
/** success **/
|
||||
--ion-color-success: #2dd36f;
|
||||
--ion-color-success-rgb: 45, 211, 111;
|
||||
--ion-color-success-contrast: #ffffff;
|
||||
--ion-color-success-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-success-shade: #28ba62;
|
||||
--ion-color-success-tint: #42d77d;
|
||||
|
||||
/** warning **/
|
||||
--ion-color-warning: #ffc409;
|
||||
--ion-color-warning-rgb: 255, 196, 9;
|
||||
--ion-color-warning-contrast: #000000;
|
||||
--ion-color-warning-contrast-rgb: 0, 0, 0;
|
||||
--ion-color-warning-shade: #e0ac08;
|
||||
--ion-color-warning-tint: #ffca22;
|
||||
|
||||
/** danger **/
|
||||
--ion-color-danger: #eb445a;
|
||||
--ion-color-danger-rgb: 235, 68, 90;
|
||||
--ion-color-danger-contrast: #ffffff;
|
||||
--ion-color-danger-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-danger-shade: #cf3c4f;
|
||||
--ion-color-danger-tint: #ed576b;
|
||||
|
||||
/** dark **/
|
||||
--ion-color-dark: #222428;
|
||||
--ion-color-dark-rgb: 34, 36, 40;
|
||||
--ion-color-dark-contrast: #ffffff;
|
||||
--ion-color-dark-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-dark-shade: #1e2023;
|
||||
--ion-color-dark-tint: #383a3e;
|
||||
|
||||
/** medium **/
|
||||
--ion-color-medium: #92949c;
|
||||
--ion-color-medium-rgb: 146, 148, 156;
|
||||
--ion-color-medium-contrast: #ffffff;
|
||||
--ion-color-medium-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-medium-shade: #808289;
|
||||
--ion-color-medium-tint: #9d9fa6;
|
||||
|
||||
/** light **/
|
||||
--ion-color-light: #f4f5f8;
|
||||
--ion-color-light-rgb: 244, 245, 248;
|
||||
--ion-color-light-contrast: #000000;
|
||||
--ion-color-light-contrast-rgb: 0, 0, 0;
|
||||
--ion-color-light-shade: #d7d8da;
|
||||
--ion-color-light-tint: #f5f6f9;
|
||||
}
|
||||
}
|
@@ -48,23 +48,33 @@ import {
|
||||
alertOutline,
|
||||
apps,
|
||||
appsOutline,
|
||||
book,
|
||||
car,
|
||||
cart,
|
||||
chatbubbleEllipses,
|
||||
chatbubbleOutline,
|
||||
chevronBackOutline,
|
||||
chevronForward,
|
||||
chevronForwardOutline,
|
||||
createOutline,
|
||||
document,
|
||||
documentTextOutline,
|
||||
gift,
|
||||
giftOutline,
|
||||
globeSharp,
|
||||
heart,
|
||||
languageOutline,
|
||||
layers,
|
||||
listCircle,
|
||||
menuOutline,
|
||||
people,
|
||||
person,
|
||||
restaurant,
|
||||
settingsOutline,
|
||||
shareSocialOutline,
|
||||
statsChart,
|
||||
sunny,
|
||||
swapHorizontal,
|
||||
trashOutline,
|
||||
} from 'ionicons/icons';
|
||||
import AboutPopover from '../../components/AboutPopover';
|
||||
@@ -87,13 +97,7 @@ interface DispatchProps {
|
||||
|
||||
interface SettingsProps extends OwnProps, StateProps, DispatchProps {}
|
||||
|
||||
const SettingsPage: React.FC<SettingsProps> = ({
|
||||
speakers,
|
||||
speakerSessions,
|
||||
logoutUser,
|
||||
setAccessToken,
|
||||
setIsLoggedIn,
|
||||
}) => {
|
||||
const SettingsPage: React.FC<SettingsProps> = ({ speakers, speakerSessions, logoutUser, setAccessToken, setIsLoggedIn }) => {
|
||||
const [events, setEvents] = useState<Event[] | []>([]);
|
||||
const [showPopover, setShowPopover] = useState(false);
|
||||
const [popoverEvent, setPopoverEvent] = useState<MouseEvent>();
|
||||
@@ -190,6 +194,72 @@ const SettingsPage: React.FC<SettingsProps> = ({
|
||||
</IonItem>
|
||||
</IonList>
|
||||
|
||||
{/* */}
|
||||
{/* */}
|
||||
{/* */}
|
||||
{/* */}
|
||||
{/* */}
|
||||
{/* */}
|
||||
{/* */}
|
||||
{/* */}
|
||||
<IonList inset={false}>
|
||||
<IonItem button={true} onClick={() => router.push(paths.DEMO_REACT_WHATSAPP_CLONE, 'forward')}>
|
||||
<IonIcon slot="start" icon={chatbubbleEllipses} size="large"></IonIcon>
|
||||
<IonLabel>
|
||||
Demo React WhatsApp Clone <span style={{ fontWeight: 'bold' }}>(need to resolve path problem)</span>
|
||||
</IonLabel>
|
||||
<IonIcon icon={chevronForwardOutline}></IonIcon>
|
||||
</IonItem>
|
||||
</IonList>
|
||||
|
||||
<IonList inset={false}>
|
||||
<IonItem button={true} onClick={() => router.push(paths.DEMO_REACT_POLL_APP, 'forward')}>
|
||||
<IonIcon slot="start" icon={statsChart} size="large"></IonIcon>
|
||||
<IonLabel>
|
||||
Demo React Poll App <span style={{ fontWeight: 'bold' }}>(css temporary broken, ignored)</span>
|
||||
</IonLabel>
|
||||
<IonIcon icon={chevronForwardOutline}></IonIcon>
|
||||
</IonItem>
|
||||
</IonList>
|
||||
|
||||
<IonList inset={false}>
|
||||
<IonItem button={true} onClick={() => router.push(paths.DEMO_REACT_SWITCH_TABS, 'forward')}>
|
||||
<IonIcon slot="start" icon={swapHorizontal} size="large"></IonIcon>
|
||||
<IonLabel>
|
||||
Demo React Switch Tabs <span style={{ fontWeight: 'bold' }}>(hardcoded back button)</span>
|
||||
</IonLabel>
|
||||
<IonIcon icon={chevronForwardOutline}></IonIcon>
|
||||
</IonItem>
|
||||
</IonList>
|
||||
|
||||
<IonList inset={false}>
|
||||
<IonItem button={true} onClick={() => router.push(paths.DEMO_REACT_OVERLAY_HOOKS, 'forward')}>
|
||||
<IonIcon slot="start" icon={layers} size="large"></IonIcon>
|
||||
<IonLabel>Demo React Overlay Hooks</IonLabel>
|
||||
<IonIcon icon={chevronForwardOutline}></IonIcon>
|
||||
</IonItem>
|
||||
</IonList>
|
||||
|
||||
<IonList inset={false}>
|
||||
<IonItem button={true} onClick={() => router.push(paths.DEMO_PINTEREST_FLOATING_TAB_BAR, 'forward')}>
|
||||
<IonIcon slot="start" icon={people} size="large"></IonIcon>
|
||||
<IonLabel>
|
||||
Demo Pinterest Floating Tab Bar <span style={{ fontWeight: 'bold' }}>(css not work well)</span>
|
||||
</IonLabel>
|
||||
<IonIcon icon={chevronForwardOutline}></IonIcon>
|
||||
</IonItem>
|
||||
</IonList>
|
||||
|
||||
<IonList inset={false}>
|
||||
<IonItem button={true} onClick={() => router.push(paths.DEMO_RESTAURANT_FINDER, 'forward')}>
|
||||
<IonIcon slot="start" icon={restaurant} size="large"></IonIcon>
|
||||
<IonLabel>
|
||||
Demo Restaurant Finder <span style={{ fontWeight: 'bold' }}>need server for map showing</span>
|
||||
</IonLabel>
|
||||
<IonIcon icon={chevronForwardOutline}></IonIcon>
|
||||
</IonItem>
|
||||
</IonList>
|
||||
|
||||
<IonList inset={false}>
|
||||
<IonItem button={true} onClick={() => handleDemoReactShopClick()}>
|
||||
<IonIcon slot="start" icon={cart} size="large"></IonIcon>
|
||||
@@ -227,8 +297,6 @@ const SettingsPage: React.FC<SettingsProps> = ({
|
||||
</IonItem>
|
||||
</IonList>
|
||||
|
||||
{/* */}
|
||||
|
||||
<IonList inset={false}>
|
||||
<IonItem button={true} onClick={() => router.push(paths.DEMO_QUOTE_APP, 'forward')}>
|
||||
<IonIcon slot="start" icon={car} size="large"></IonIcon>
|
||||
@@ -261,7 +329,6 @@ const SettingsPage: React.FC<SettingsProps> = ({
|
||||
</IonItem>
|
||||
</IonList>
|
||||
|
||||
{/* */}
|
||||
<IonList inset={false}>
|
||||
<IonItem button={true} onClick={() => router.push(paths.DEMO_RECIPE_APP, 'forward')}>
|
||||
<IonIcon slot="start" icon={cart} size="large"></IonIcon>
|
||||
@@ -269,14 +336,44 @@ const SettingsPage: React.FC<SettingsProps> = ({
|
||||
<IonIcon icon={chevronForwardOutline}></IonIcon>
|
||||
</IonItem>
|
||||
</IonList>
|
||||
|
||||
<IonList inset={false}>
|
||||
<IonItem button={true} onClick={() => router.push(paths.DEMO_SLIDING_PROFILE, 'forward')}>
|
||||
<IonIcon slot="start" icon={person} size="large"></IonIcon>
|
||||
<IonLabel>Demo Sliding Profile</IonLabel>
|
||||
<IonIcon icon={chevronForwardOutline}></IonIcon>
|
||||
</IonItem>
|
||||
</IonList>
|
||||
|
||||
<IonList inset={false}>
|
||||
<IonItem button={true} onClick={() => router.push(paths.DEMO_QUIZ_APP, 'forward')}>
|
||||
<IonIcon slot="start" icon={book} size="large"></IonIcon>
|
||||
<IonLabel>Demo Quiz App</IonLabel>
|
||||
<IonIcon icon={chevronForwardOutline}></IonIcon>
|
||||
</IonItem>
|
||||
</IonList>
|
||||
|
||||
<IonList inset={false}>
|
||||
<IonItem button={true} onClick={() => router.push(paths.DEMO_BLOG_POST_UI, 'forward')}>
|
||||
<IonIcon slot="start" icon={document} size="large"></IonIcon>
|
||||
<IonLabel>Demo Blog Post UI</IonLabel>
|
||||
<IonIcon icon={chevronForwardOutline}></IonIcon>
|
||||
</IonItem>
|
||||
</IonList>
|
||||
|
||||
<IonList inset={false}>
|
||||
<IonItem button={true} onClick={() => router.push(paths.DEMO_REACT_TRAVEL_APP, 'forward')}>
|
||||
<IonIcon slot="start" icon={globeSharp} size="large"></IonIcon>
|
||||
<IonLabel>
|
||||
Demo React Travel App <span style={{ fontWeight: 'bold' }}>(on hold)</span>
|
||||
</IonLabel>
|
||||
<IonIcon icon={chevronForwardOutline}></IonIcon>
|
||||
</IonItem>
|
||||
</IonList>
|
||||
</IonContent>
|
||||
|
||||
{/* REQ0058/logout */}
|
||||
<IonModal
|
||||
isOpen={showLogoutConfirmModal}
|
||||
initialBreakpoint={0.5}
|
||||
breakpoints={[0, 0.25, 0.5, 0.75]}
|
||||
>
|
||||
<IonModal isOpen={showLogoutConfirmModal} initialBreakpoint={0.5} breakpoints={[0, 0.25, 0.5, 0.75]}>
|
||||
<IonContent
|
||||
className="ion-padding"
|
||||
style={{
|
||||
|
@@ -0,0 +1,49 @@
|
||||
import {
|
||||
IonButton,
|
||||
IonButtons,
|
||||
IonContent,
|
||||
IonHeader,
|
||||
IonIcon,
|
||||
IonPage,
|
||||
IonTitle,
|
||||
IonToolbar,
|
||||
useIonRouter,
|
||||
} from '@ionic/react';
|
||||
import ExploreContainer from '../components/ExploreContainer';
|
||||
|
||||
import './Tab1.css';
|
||||
import { chevronBackOutline } from 'ionicons/icons';
|
||||
|
||||
const Tab1 = () => {
|
||||
const router = useIonRouter();
|
||||
|
||||
function handleBackClick() {
|
||||
router.goBack();
|
||||
}
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonTitle>Tab 1</IonTitle>
|
||||
{/* */}
|
||||
<IonButtons slot="start">
|
||||
<IonButton onClick={() => handleBackClick()}>
|
||||
<IonIcon icon={chevronBackOutline} color="primary" />
|
||||
</IonButton>
|
||||
</IonButtons>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<IonContent fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large">Tab 1</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<ExploreContainer name="Tab 1 page" />
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tab1;
|
@@ -0,0 +1,25 @@
|
||||
import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar } from '@ionic/react';
|
||||
import ExploreContainer from '../components/ExploreContainer';
|
||||
import './Tab2.css';
|
||||
|
||||
const Tab2 = () => {
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonTitle>Tab 2</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<IonContent fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large">Tab 2</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<ExploreContainer name="Tab 2 page" />
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tab2;
|
@@ -0,0 +1,25 @@
|
||||
import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar } from '@ionic/react';
|
||||
import ExploreContainer from '../components/ExploreContainer';
|
||||
import './Tab3.css';
|
||||
|
||||
const Tab3 = () => {
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonTitle>Tab 3</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<IonContent fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large">Tab 3</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<ExploreContainer name="Tab 3 page" />
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tab3;
|
@@ -0,0 +1,25 @@
|
||||
import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar } from '@ionic/react';
|
||||
import ExploreContainer from '../components/ExploreContainer';
|
||||
import './Tab3.css';
|
||||
|
||||
const Tab4 = () => {
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonTitle>Tab 3</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<IonContent fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large">Tab 4</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<ExploreContainer name="Tab 4 page" />
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tab4;
|
@@ -0,0 +1,24 @@
|
||||
.container {
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.container strong {
|
||||
font-size: 20px;
|
||||
line-height: 26px;
|
||||
}
|
||||
|
||||
.container p {
|
||||
font-size: 16px;
|
||||
line-height: 22px;
|
||||
color: #8c8c8c;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.container a {
|
||||
text-decoration: none;
|
||||
}
|
@@ -0,0 +1,12 @@
|
||||
import './ExploreContainer.css';
|
||||
|
||||
const ExploreContainer = ({ name }) => {
|
||||
return (
|
||||
<div className="container">
|
||||
<strong>{name}</strong>
|
||||
<p>Explore <a target="_blank" rel="noopener noreferrer" href="https://ionicframework.com/docs/components">UI Components</a></p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExploreContainer;
|
@@ -0,0 +1,29 @@
|
||||
.custom-tab-bar {
|
||||
* {
|
||||
/* --ion-background-color: white; */
|
||||
--ion-tab-bar-color: var(--tab-color);
|
||||
--ion-tab-bar-color-selected: var(--tab-color-selected);
|
||||
}
|
||||
|
||||
ion-tab-bar {
|
||||
--background: var(--tab-background);
|
||||
box-shadow: 0px 1px 8px rgba(0, 0, 0, 0.4);
|
||||
border-radius: 50px !important;
|
||||
|
||||
height: 50px;
|
||||
width: 50%;
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
|
||||
bottom: 20px;
|
||||
position: relative;
|
||||
margin: 0 auto !important;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
ion-tab-button {
|
||||
border-radius: 16px !important;
|
||||
}
|
||||
}
|
@@ -0,0 +1,54 @@
|
||||
import { IonIcon, IonLabel, IonRouterOutlet, IonTabBar, IonTabButton, IonTabs } from '@ionic/react';
|
||||
|
||||
import { chatbubble, cloudOutline, home, person, search, searchOutline } from 'ionicons/icons';
|
||||
import { Route, Redirect } from 'react-router';
|
||||
|
||||
import Tab1 from './AppPages/Tab1';
|
||||
import Tab2 from './AppPages/Tab2';
|
||||
import Tab3 from './AppPages/Tab3';
|
||||
import Tab4 from './AppPages/Tab4';
|
||||
|
||||
import './style.scss';
|
||||
import './custom-tab-bar.scss';
|
||||
|
||||
function DemoPinterestFloatingTabBar() {
|
||||
return (
|
||||
<IonTabs>
|
||||
<IonRouterOutlet className="demo-pinterest-floating-tab-bar">
|
||||
<Route exact path="/demo-pinterest-floating-tab-bar/tab1">
|
||||
<Tab1 />
|
||||
</Route>
|
||||
<Route exact path="/demo-pinterest-floating-tab-bar/tab2">
|
||||
<Tab2 />
|
||||
</Route>
|
||||
<Route path="/demo-pinterest-floating-tab-bar/tab3">
|
||||
<Tab3 />
|
||||
</Route>
|
||||
<Route path="/demo-pinterest-floating-tab-bar/tab4">
|
||||
<Tab4 />
|
||||
</Route>
|
||||
<Route exact path="/demo-pinterest-floating-tab-bar/">
|
||||
<Redirect to="/tab1" />
|
||||
</Route>
|
||||
</IonRouterOutlet>
|
||||
{/* */}
|
||||
<IonTabBar slot="bottom">
|
||||
<IonTabButton tab="tab1" href="/demo-pinterest-floating-tab-bar/tab1">
|
||||
<IonIcon icon={home} />
|
||||
</IonTabButton>
|
||||
<IonTabButton tab="tab2" href="/demo-pinterest-floating-tab-bar/tab2">
|
||||
<IonIcon icon={search} />
|
||||
</IonTabButton>
|
||||
<IonTabButton tab="tab3" href="/demo-pinterest-floating-tab-bar/tab3">
|
||||
<IonIcon icon={chatbubble} />
|
||||
</IonTabButton>
|
||||
|
||||
<IonTabButton tab="tab4" href="/demo-pinterest-floating-tab-bar/tab4">
|
||||
<IonIcon icon={person} />
|
||||
</IonTabButton>
|
||||
</IonTabBar>
|
||||
</IonTabs>
|
||||
);
|
||||
}
|
||||
|
||||
export default DemoPinterestFloatingTabBar;
|
@@ -0,0 +1,253 @@
|
||||
/* Ionic Variables and Theming. For more info, please see:
|
||||
http://ionicframework.com/docs/theming/ */
|
||||
|
||||
/** Ionic CSS Variables **/
|
||||
.demo-pinterest-floating-tab-bar {
|
||||
* {
|
||||
/** primary **/
|
||||
--ion-color-primary: #3880ff;
|
||||
--ion-color-primary-rgb: 56, 128, 255;
|
||||
--ion-color-primary-contrast: #ffffff;
|
||||
--ion-color-primary-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-primary-shade: #3171e0;
|
||||
--ion-color-primary-tint: #4c8dff;
|
||||
|
||||
/** secondary **/
|
||||
--ion-color-secondary: #3dc2ff;
|
||||
--ion-color-secondary-rgb: 61, 194, 255;
|
||||
--ion-color-secondary-contrast: #ffffff;
|
||||
--ion-color-secondary-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-secondary-shade: #36abe0;
|
||||
--ion-color-secondary-tint: #50c8ff;
|
||||
|
||||
/** tertiary **/
|
||||
--ion-color-tertiary: #5260ff;
|
||||
--ion-color-tertiary-rgb: 82, 96, 255;
|
||||
--ion-color-tertiary-contrast: #ffffff;
|
||||
--ion-color-tertiary-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-tertiary-shade: #4854e0;
|
||||
--ion-color-tertiary-tint: #6370ff;
|
||||
|
||||
/** success **/
|
||||
--ion-color-success: #2dd36f;
|
||||
--ion-color-success-rgb: 45, 211, 111;
|
||||
--ion-color-success-contrast: #ffffff;
|
||||
--ion-color-success-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-success-shade: #28ba62;
|
||||
--ion-color-success-tint: #42d77d;
|
||||
|
||||
/** warning **/
|
||||
--ion-color-warning: #ffc409;
|
||||
--ion-color-warning-rgb: 255, 196, 9;
|
||||
--ion-color-warning-contrast: #000000;
|
||||
--ion-color-warning-contrast-rgb: 0, 0, 0;
|
||||
--ion-color-warning-shade: #e0ac08;
|
||||
--ion-color-warning-tint: #ffca22;
|
||||
|
||||
/** danger **/
|
||||
--ion-color-danger: #eb445a;
|
||||
--ion-color-danger-rgb: 235, 68, 90;
|
||||
--ion-color-danger-contrast: #ffffff;
|
||||
--ion-color-danger-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-danger-shade: #cf3c4f;
|
||||
--ion-color-danger-tint: #ed576b;
|
||||
|
||||
/** dark **/
|
||||
--ion-color-dark: #222428;
|
||||
--ion-color-dark-rgb: 34, 36, 40;
|
||||
--ion-color-dark-contrast: #ffffff;
|
||||
--ion-color-dark-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-dark-shade: #1e2023;
|
||||
--ion-color-dark-tint: #383a3e;
|
||||
|
||||
/** medium **/
|
||||
--ion-color-medium: #92949c;
|
||||
--ion-color-medium-rgb: 146, 148, 156;
|
||||
--ion-color-medium-contrast: #ffffff;
|
||||
--ion-color-medium-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-medium-shade: #808289;
|
||||
--ion-color-medium-tint: #9d9fa6;
|
||||
|
||||
/** light **/
|
||||
--ion-color-light: #f4f5f8;
|
||||
--ion-color-light-rgb: 244, 245, 248;
|
||||
--ion-color-light-contrast: #000000;
|
||||
--ion-color-light-contrast-rgb: 0, 0, 0;
|
||||
--ion-color-light-shade: #d7d8da;
|
||||
--ion-color-light-tint: #f5f6f9;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
/*
|
||||
* Dark Colors
|
||||
* -------------------------------------------
|
||||
*/
|
||||
|
||||
body {
|
||||
--ion-color-primary: #428cff;
|
||||
--ion-color-primary-rgb: 66, 140, 255;
|
||||
--ion-color-primary-contrast: #ffffff;
|
||||
--ion-color-primary-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-primary-shade: #3a7be0;
|
||||
--ion-color-primary-tint: #5598ff;
|
||||
|
||||
--ion-color-secondary: #50c8ff;
|
||||
--ion-color-secondary-rgb: 80, 200, 255;
|
||||
--ion-color-secondary-contrast: #ffffff;
|
||||
--ion-color-secondary-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-secondary-shade: #46b0e0;
|
||||
--ion-color-secondary-tint: #62ceff;
|
||||
|
||||
--ion-color-tertiary: #6a64ff;
|
||||
--ion-color-tertiary-rgb: 106, 100, 255;
|
||||
--ion-color-tertiary-contrast: #ffffff;
|
||||
--ion-color-tertiary-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-tertiary-shade: #5d58e0;
|
||||
--ion-color-tertiary-tint: #7974ff;
|
||||
|
||||
--ion-color-success: #2fdf75;
|
||||
--ion-color-success-rgb: 47, 223, 117;
|
||||
--ion-color-success-contrast: #000000;
|
||||
--ion-color-success-contrast-rgb: 0, 0, 0;
|
||||
--ion-color-success-shade: #29c467;
|
||||
--ion-color-success-tint: #44e283;
|
||||
|
||||
--ion-color-warning: #ffd534;
|
||||
--ion-color-warning-rgb: 255, 213, 52;
|
||||
--ion-color-warning-contrast: #000000;
|
||||
--ion-color-warning-contrast-rgb: 0, 0, 0;
|
||||
--ion-color-warning-shade: #e0bb2e;
|
||||
--ion-color-warning-tint: #ffd948;
|
||||
|
||||
--ion-color-danger: #ff4961;
|
||||
--ion-color-danger-rgb: 255, 73, 97;
|
||||
--ion-color-danger-contrast: #ffffff;
|
||||
--ion-color-danger-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-danger-shade: #e04055;
|
||||
--ion-color-danger-tint: #ff5b71;
|
||||
|
||||
--ion-color-dark: #f4f5f8;
|
||||
--ion-color-dark-rgb: 244, 245, 248;
|
||||
--ion-color-dark-contrast: #000000;
|
||||
--ion-color-dark-contrast-rgb: 0, 0, 0;
|
||||
--ion-color-dark-shade: #d7d8da;
|
||||
--ion-color-dark-tint: #f5f6f9;
|
||||
|
||||
--ion-color-medium: #989aa2;
|
||||
--ion-color-medium-rgb: 152, 154, 162;
|
||||
--ion-color-medium-contrast: #000000;
|
||||
--ion-color-medium-contrast-rgb: 0, 0, 0;
|
||||
--ion-color-medium-shade: #86888f;
|
||||
--ion-color-medium-tint: #a2a4ab;
|
||||
|
||||
--ion-color-light: #222428;
|
||||
--ion-color-light-rgb: 34, 36, 40;
|
||||
--ion-color-light-contrast: #ffffff;
|
||||
--ion-color-light-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-light-shade: #1e2023;
|
||||
--ion-color-light-tint: #383a3e;
|
||||
}
|
||||
|
||||
/*
|
||||
* iOS Dark Theme
|
||||
* -------------------------------------------
|
||||
*/
|
||||
|
||||
.ios body {
|
||||
--ion-background-color: #000000;
|
||||
--ion-background-color-rgb: 0, 0, 0;
|
||||
|
||||
--ion-text-color: #ffffff;
|
||||
--ion-text-color-rgb: 255, 255, 255;
|
||||
|
||||
--ion-color-step-50: #0d0d0d;
|
||||
--ion-color-step-100: #1a1a1a;
|
||||
--ion-color-step-150: #262626;
|
||||
--ion-color-step-200: #333333;
|
||||
--ion-color-step-250: #404040;
|
||||
--ion-color-step-300: #4d4d4d;
|
||||
--ion-color-step-350: #595959;
|
||||
--ion-color-step-400: #666666;
|
||||
--ion-color-step-450: #737373;
|
||||
--ion-color-step-500: #808080;
|
||||
--ion-color-step-550: #8c8c8c;
|
||||
--ion-color-step-600: #999999;
|
||||
--ion-color-step-650: #a6a6a6;
|
||||
--ion-color-step-700: #b3b3b3;
|
||||
--ion-color-step-750: #bfbfbf;
|
||||
--ion-color-step-800: #cccccc;
|
||||
--ion-color-step-850: #d9d9d9;
|
||||
--ion-color-step-900: #e6e6e6;
|
||||
--ion-color-step-950: #f2f2f2;
|
||||
|
||||
--ion-item-background: #000000;
|
||||
|
||||
--ion-card-background: #1c1c1d;
|
||||
}
|
||||
|
||||
.ios ion-modal {
|
||||
--ion-background-color: var(--ion-color-step-100);
|
||||
--ion-toolbar-background: var(--ion-color-step-150);
|
||||
--ion-toolbar-border-color: var(--ion-color-step-250);
|
||||
}
|
||||
|
||||
/*
|
||||
* Material Design Dark Theme
|
||||
* -------------------------------------------
|
||||
*/
|
||||
|
||||
.md body {
|
||||
--ion-background-color: #121212;
|
||||
--ion-background-color-rgb: 18, 18, 18;
|
||||
|
||||
--ion-text-color: #ffffff;
|
||||
--ion-text-color-rgb: 255, 255, 255;
|
||||
|
||||
--ion-border-color: #222222;
|
||||
|
||||
--ion-color-step-50: #1e1e1e;
|
||||
--ion-color-step-100: #2a2a2a;
|
||||
--ion-color-step-150: #363636;
|
||||
--ion-color-step-200: #414141;
|
||||
--ion-color-step-250: #4d4d4d;
|
||||
--ion-color-step-300: #595959;
|
||||
--ion-color-step-350: #656565;
|
||||
--ion-color-step-400: #717171;
|
||||
--ion-color-step-450: #7d7d7d;
|
||||
--ion-color-step-500: #898989;
|
||||
--ion-color-step-550: #949494;
|
||||
--ion-color-step-600: #a0a0a0;
|
||||
--ion-color-step-650: #acacac;
|
||||
--ion-color-step-700: #b8b8b8;
|
||||
--ion-color-step-750: #c4c4c4;
|
||||
--ion-color-step-800: #d0d0d0;
|
||||
--ion-color-step-850: #dbdbdb;
|
||||
--ion-color-step-900: #e7e7e7;
|
||||
--ion-color-step-950: #f3f3f3;
|
||||
|
||||
--ion-item-background: #1e1e1e;
|
||||
|
||||
--ion-toolbar-background: #1f1f1f;
|
||||
|
||||
--ion-tab-bar-background: #1f1f1f;
|
||||
|
||||
--ion-card-background: #1e1e1e;
|
||||
}
|
||||
}
|
||||
|
||||
:root {
|
||||
/* Custom tab bar */
|
||||
--tab-background: rgb(251, 251, 251);
|
||||
--tab-color: rgb(153, 153, 153);
|
||||
--tab-color-selected: black;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
/* Custom tab bar */
|
||||
--tab-background: rgb(53, 53, 53);
|
||||
--tab-color: rgb(83, 83, 83);
|
||||
--tab-color-selected: white;
|
||||
}
|
||||
}
|
||||
}
|
63
03_source/mobile/src/pages/DemoQuizApp/AppPages/Home.jsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import {
|
||||
IonButton,
|
||||
IonCol,
|
||||
IonContent,
|
||||
IonGrid,
|
||||
IonHeader,
|
||||
IonIcon,
|
||||
IonPage,
|
||||
IonRow,
|
||||
IonTitle,
|
||||
IonToolbar,
|
||||
useIonActionSheet,
|
||||
} from '@ionic/react';
|
||||
import styles from './Home.module.scss';
|
||||
|
||||
import { informationCircleOutline } from 'ionicons/icons';
|
||||
|
||||
const Home = () => {
|
||||
const [show, hide] = useIonActionSheet();
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonContent fullscreen>
|
||||
<IonGrid>
|
||||
<IonRow>
|
||||
<IonCol size="12" className="ion-text-center">
|
||||
<img src="/assets/DemoQuizApp/main.png" alt="title" className={styles.title} />
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
</IonGrid>
|
||||
<IonRow className={styles.buttons}>
|
||||
<IonCol size="12">
|
||||
<IonButton
|
||||
routerLink="/demo-quiz-app/quiz"
|
||||
color="light"
|
||||
expand="block"
|
||||
className={styles.playButton}
|
||||
>
|
||||
Start Playing
|
||||
</IonButton>
|
||||
|
||||
<IonButton
|
||||
color="dark"
|
||||
className={styles.helpButton}
|
||||
onClick={() =>
|
||||
show({
|
||||
buttons: [{ text: 'Close' }],
|
||||
header: 'How to play',
|
||||
subHeader:
|
||||
'Pick a category and difficulty, then proceed to answer each question. You will gain a score by getting an answer right and you will also be indicated whether your answer was correct or incorrect. Have fun!',
|
||||
})
|
||||
}
|
||||
>
|
||||
<IonIcon icon={informationCircleOutline} /> How to play
|
||||
</IonButton>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
@@ -0,0 +1,43 @@
|
||||
.title {
|
||||
|
||||
height: 10rem;
|
||||
margin-top: 30%;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
|
||||
position: absolute;
|
||||
bottom: 3rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.playButton {
|
||||
|
||||
height: 4rem;
|
||||
--border-radius: 500px;
|
||||
width: fit-content;
|
||||
--padding-start: 5rem;
|
||||
--padding-end: 5rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.helpButton {
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-content: center;
|
||||
text-align: center;
|
||||
width: fit-content;
|
||||
margin: 0 auto;
|
||||
margin-top: 3rem;
|
||||
opacity: 70%;
|
||||
--border-radius: 10rem !important;
|
||||
--padding-end: 1.25rem;
|
||||
|
||||
ion-icon {
|
||||
|
||||
margin-top: 0.2rem;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
193
03_source/mobile/src/pages/DemoQuizApp/AppPages/Questions.jsx
Normal file
@@ -0,0 +1,193 @@
|
||||
import {
|
||||
IonBadge,
|
||||
IonButton,
|
||||
IonCard,
|
||||
IonCardContent,
|
||||
IonCardHeader,
|
||||
IonCardSubtitle,
|
||||
IonCardTitle,
|
||||
IonCol,
|
||||
IonContent,
|
||||
IonGrid,
|
||||
IonHeader,
|
||||
IonIcon,
|
||||
IonItem,
|
||||
IonLabel,
|
||||
IonNote,
|
||||
IonPage,
|
||||
IonRow,
|
||||
IonTitle,
|
||||
IonToolbar,
|
||||
useIonRouter,
|
||||
useIonViewDidEnter,
|
||||
} from '@ionic/react';
|
||||
|
||||
import styles from './Quiz.module.scss';
|
||||
import { useStoreState } from 'pullstate';
|
||||
import { SettingsStore } from '../store';
|
||||
import {
|
||||
getCategories,
|
||||
getChosenCategory,
|
||||
getChosenDifficulty,
|
||||
getDifficulties,
|
||||
} from '../store/Selectors';
|
||||
import { Category, Difficulty } from '../components/Settings';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { fetchQuestions } from '../questions';
|
||||
|
||||
// Import Swiper React components
|
||||
import { Swiper, SwiperSlide } from 'swiper/react';
|
||||
|
||||
// Import Swiper styles
|
||||
// import 'swiper/swiper.scss';
|
||||
import 'swiper/css';
|
||||
|
||||
import { useRef } from 'react';
|
||||
import { updateChosenCategory, updateChosenDifficulty } from '../store/SettingsStore';
|
||||
import { Answer } from '../components/Answer';
|
||||
import { CompletedCard } from '../components/CompletedCard';
|
||||
import { QuizStats } from '../components/QuizStats';
|
||||
|
||||
const Questions = () => {
|
||||
const mainContainerRef = useRef();
|
||||
const completionContainerRef = useRef();
|
||||
const swiperRef = useRef(null);
|
||||
|
||||
const router = useIonRouter();
|
||||
const chosenCategory = useStoreState(SettingsStore, getChosenCategory);
|
||||
const chosenDifficulty = useStoreState(SettingsStore, getChosenDifficulty);
|
||||
|
||||
const [currentQuestion, setCurrentQuestion] = useState(1);
|
||||
const [score, setScore] = useState(0);
|
||||
const [completed, setCompleted] = useState(false);
|
||||
|
||||
const [questions, setQuestions] = useState(false);
|
||||
const [slideSpace, setSlideSpace] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const getQuestions = async () => {
|
||||
const fetchedQuestions = await fetchQuestions(chosenCategory, chosenDifficulty);
|
||||
setQuestions(fetchedQuestions);
|
||||
};
|
||||
|
||||
getQuestions();
|
||||
}, []);
|
||||
|
||||
useIonViewDidEnter(() => {
|
||||
setSlideSpace(40);
|
||||
});
|
||||
|
||||
const handleAnswerClick = (event, answer, question) => {
|
||||
const isCorrect = question.correct_answers[`${answer}_correct`] === 'true';
|
||||
|
||||
if (isCorrect) {
|
||||
event.target.setAttribute('color', 'success');
|
||||
} else {
|
||||
event.target.setAttribute('color', 'danger');
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
isCorrect && setScore((score) => score + 1);
|
||||
event.target.setAttribute('color', 'light');
|
||||
swiperRef.current.swiper.slideNext();
|
||||
checkIfComplete();
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const checkIfComplete = () => {
|
||||
if (currentQuestion === questions.length) {
|
||||
// Quiz has finished
|
||||
// Hide Slides and show completion screen
|
||||
mainContainerRef.current.classList.add('animate__zoomOutDown');
|
||||
|
||||
setTimeout(() => {
|
||||
setCompleted(true);
|
||||
completionContainerRef.current.classList.add('animate__zoomInUp');
|
||||
}, 1000);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonTitle>
|
||||
<img src="/assets/DemoQuizApp/main.png" style={{ width: '30%' }} alt="logo" />
|
||||
</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<IonContent fullscreen className="background">
|
||||
{!completed && (
|
||||
<IonGrid className={`${styles.mainGrid} animate__animated`} ref={mainContainerRef}>
|
||||
<QuizStats
|
||||
chosenCategory={chosenCategory}
|
||||
chosenDifficulty={chosenDifficulty}
|
||||
questionsLength={questions.length}
|
||||
currentQuestion={currentQuestion}
|
||||
score={score}
|
||||
/>
|
||||
|
||||
<IonRow className={styles.mainRow}>
|
||||
<IonCol size="12">
|
||||
<IonRow>
|
||||
<Swiper
|
||||
ref={swiperRef}
|
||||
spaceBetween={slideSpace}
|
||||
slidesPerView={1}
|
||||
onSlideChange={(e) => setCurrentQuestion(e.activeIndex + 1)}
|
||||
>
|
||||
{questions &&
|
||||
questions.map((question, index) => {
|
||||
return (
|
||||
<SwiperSlide key={`question_${index}`}>
|
||||
<IonCard id="questionCard" className="animate__animated">
|
||||
<IonCardHeader className="ion-text-center">
|
||||
<IonCardSubtitle>{question.category}</IonCardSubtitle>
|
||||
{question.tags.length > 0 && (
|
||||
<IonBadge color="success">{question.tags[0].name}</IonBadge>
|
||||
)}
|
||||
<IonCardTitle className={styles.questionTitle}>
|
||||
{question.question}
|
||||
</IonCardTitle>
|
||||
</IonCardHeader>
|
||||
|
||||
<IonCardContent>
|
||||
{Object.keys(question.answers).map((answer, index) => {
|
||||
if (question.answers[answer] !== null) {
|
||||
return (
|
||||
<Answer
|
||||
key={`answer_${index}`}
|
||||
answer={answer}
|
||||
question={question}
|
||||
handleAnswerClick={handleAnswerClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</IonCardContent>
|
||||
</IonCard>
|
||||
</SwiperSlide>
|
||||
);
|
||||
})}
|
||||
</Swiper>
|
||||
</IonRow>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
</IonGrid>
|
||||
)}
|
||||
|
||||
{completed && (
|
||||
<CompletedCard
|
||||
completionContainerRef={completionContainerRef}
|
||||
score={score}
|
||||
questionsLength={questions.length}
|
||||
/>
|
||||
)}
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Questions;
|
153
03_source/mobile/src/pages/DemoQuizApp/AppPages/Quiz.jsx
Normal file
@@ -0,0 +1,153 @@
|
||||
import {
|
||||
IonButton,
|
||||
IonCard,
|
||||
IonCardContent,
|
||||
IonCardHeader,
|
||||
IonCardSubtitle,
|
||||
IonCol,
|
||||
IonContent,
|
||||
IonGrid,
|
||||
IonHeader,
|
||||
IonIcon,
|
||||
IonPage,
|
||||
IonRow,
|
||||
IonTitle,
|
||||
IonToolbar,
|
||||
useIonRouter,
|
||||
useIonToast,
|
||||
} from '@ionic/react';
|
||||
|
||||
import styles from './Quiz.module.scss';
|
||||
import { useStoreState } from 'pullstate';
|
||||
import { SettingsStore } from '../store';
|
||||
import {
|
||||
getCategories,
|
||||
getChosenCategory,
|
||||
getChosenDifficulty,
|
||||
getDifficulties,
|
||||
} from '../store/Selectors';
|
||||
import { Category, Difficulty } from '../components/Settings';
|
||||
|
||||
const Quiz = () => {
|
||||
const router = useIonRouter();
|
||||
const categories = useStoreState(SettingsStore, getCategories);
|
||||
const difficulties = useStoreState(SettingsStore, getDifficulties);
|
||||
|
||||
const chosenCategory = useStoreState(SettingsStore, getChosenCategory);
|
||||
const chosenDifficulty = useStoreState(SettingsStore, getChosenDifficulty);
|
||||
|
||||
const [show, hide] = useIonToast();
|
||||
|
||||
const startQuiz = async () => {
|
||||
if (chosenCategory && chosenDifficulty) {
|
||||
const chosenCategoryElement = document.getElementById(`categoryButton_${chosenCategory}`);
|
||||
const chosenDifficultyElement = document.getElementById(
|
||||
`difficultyButton_${chosenDifficulty}`
|
||||
);
|
||||
|
||||
const categoriesCardElement = document.getElementById('categoriesCard');
|
||||
const difficultiesCardElement = document.getElementById('difficultiesCard');
|
||||
|
||||
chosenCategoryElement.classList.add('ontop');
|
||||
chosenDifficultyElement.classList.add('ontop');
|
||||
|
||||
chosenCategoryElement.classList.add('animate__heartBeat');
|
||||
chosenDifficultyElement.classList.add('animate__heartBeat');
|
||||
|
||||
setTimeout(() => {
|
||||
chosenCategoryElement.classList.remove('animate__heartBeat');
|
||||
chosenDifficultyElement.classList.remove('animate__heartBeat');
|
||||
chosenCategoryElement.classList.remove('ontop');
|
||||
chosenDifficultyElement.classList.remove('ontop');
|
||||
}, 1000);
|
||||
|
||||
setTimeout(() => {
|
||||
categoriesCardElement.classList.add('animate__slideOutRight');
|
||||
difficultiesCardElement.classList.add('animate__slideOutLeft');
|
||||
|
||||
setTimeout(() => {
|
||||
categoriesCardElement.classList.remove('animate__slideOutRight');
|
||||
difficultiesCardElement.classList.remove('animate__slideOutLeft');
|
||||
}, 1000);
|
||||
}, 1100);
|
||||
|
||||
setTimeout(() => {
|
||||
router.push('/questions');
|
||||
}, 1700);
|
||||
} else {
|
||||
show({
|
||||
header: 'Hang on there!',
|
||||
message: 'You must choose a category and difficulty!',
|
||||
duration: 3000,
|
||||
color: 'warning',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonTitle>
|
||||
<img src="/assets/DemoQuizApp/main.png" style={{ width: '30%' }} alt="logo" />
|
||||
</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<IonContent fullscreen className="background">
|
||||
<IonGrid className={styles.mainGrid}>
|
||||
<IonRow className={styles.mainRow}>
|
||||
<IonCol size="12">
|
||||
<IonCard id="categoriesCard" className="animate__animated">
|
||||
<IonCardHeader className="ion-text-center">
|
||||
<IonCardSubtitle>Choose a category</IonCardSubtitle>
|
||||
</IonCardHeader>
|
||||
|
||||
<IonCardContent>
|
||||
<IonRow>
|
||||
{categories.map((category, index) => {
|
||||
const chosen = category.value === chosenCategory;
|
||||
|
||||
return <Category key={`category_${index}`} {...category} chosen={chosen} />;
|
||||
})}
|
||||
</IonRow>
|
||||
</IonCardContent>
|
||||
</IonCard>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
|
||||
<IonRow className={styles.difficultyContainer}>
|
||||
<IonCol size="12">
|
||||
<IonCard id="difficultiesCard" className="animate__animated">
|
||||
<IonCardHeader className="ion-text-center">
|
||||
<IonCardSubtitle>Choose a difficulty</IonCardSubtitle>
|
||||
</IonCardHeader>
|
||||
|
||||
<IonCardContent>
|
||||
<IonRow>
|
||||
{difficulties.map((difficulty, index) => {
|
||||
const chosen = difficulty.value === chosenDifficulty;
|
||||
|
||||
return (
|
||||
<Difficulty key={`difficulty_${index}`} {...difficulty} chosen={chosen} />
|
||||
);
|
||||
})}
|
||||
</IonRow>
|
||||
</IonCardContent>
|
||||
</IonCard>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
|
||||
<IonRow>
|
||||
<IonCol size="12">
|
||||
<div className={styles.startButton} onClick={startQuiz}>
|
||||
Start Quiz!
|
||||
</div>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
</IonGrid>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Quiz;
|
@@ -0,0 +1,46 @@
|
||||
.difficultyContainer {
|
||||
|
||||
margin-top: -2rem !important;
|
||||
}
|
||||
|
||||
.startButton {
|
||||
|
||||
background-color: #994ec1;
|
||||
padding: 1.25rem;
|
||||
margin: 1rem;
|
||||
margin-top: -1rem;
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
color: white;
|
||||
border: 2px solid #632485;
|
||||
}
|
||||
|
||||
.questionTitle {
|
||||
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.answerButton {
|
||||
|
||||
height: fit-content;
|
||||
--padding-top: 1rem;
|
||||
--padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.mainGrid {
|
||||
|
||||
// margin-top: -2rem;
|
||||
}
|
||||
|
||||
.mainRow {
|
||||
|
||||
margin-top: -2rem;
|
||||
}
|
||||
|
||||
.emoji {
|
||||
|
||||
font-size: 4rem;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
padding-top: 1rem;
|
||||
}
|
17
03_source/mobile/src/pages/DemoQuizApp/components/Answer.jsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { IonButton, IonCol, IonRow } from '@ionic/react';
|
||||
import styles from './Quiz.module.scss';
|
||||
|
||||
export const Answer = ({ answer, handleAnswerClick, question }) => (
|
||||
<IonRow>
|
||||
<IonCol size="12">
|
||||
<IonButton
|
||||
onClick={(e) => handleAnswerClick(e, answer, question)}
|
||||
expand="block"
|
||||
color="light"
|
||||
className={`ion-text-wrap ${styles.answerButton}`}
|
||||
>
|
||||
{question.answers[answer]}
|
||||
</IonButton>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
);
|
@@ -0,0 +1,58 @@
|
||||
import {
|
||||
IonButton,
|
||||
IonCard,
|
||||
IonCardContent,
|
||||
IonCardHeader,
|
||||
IonCardSubtitle,
|
||||
IonCardTitle,
|
||||
IonCol,
|
||||
IonGrid,
|
||||
IonNote,
|
||||
IonRow,
|
||||
useIonRouter,
|
||||
} from '@ionic/react';
|
||||
import styles from './Quiz.module.scss';
|
||||
import { updateChosenCategory, updateChosenDifficulty } from '../store/SettingsStore';
|
||||
|
||||
export const CompletedCard = ({ completionContainerRef, score, questionsLength }) => {
|
||||
const router = useIonRouter();
|
||||
|
||||
const playAgain = () => {
|
||||
updateChosenCategory(false);
|
||||
updateChosenDifficulty(false);
|
||||
router.push('/');
|
||||
};
|
||||
|
||||
return (
|
||||
<IonGrid className="animate__animated" ref={completionContainerRef}>
|
||||
<IonRow className="ion-text-center">
|
||||
<IonCol size="12">
|
||||
<IonCard>
|
||||
<IonCardHeader>
|
||||
<IonCardSubtitle>Congratulations</IonCardSubtitle>
|
||||
<IonCardTitle>Quiz Complete!</IonCardTitle>
|
||||
<p className={styles.emoji}>🎉</p>
|
||||
</IonCardHeader>
|
||||
|
||||
<IonCardContent>
|
||||
<IonNote>You scored</IonNote>
|
||||
|
||||
<IonCardTitle className="ion-margin-bottom">
|
||||
{score}/{questionsLength}
|
||||
</IonCardTitle>
|
||||
|
||||
<IonButton
|
||||
onClick={playAgain}
|
||||
color="success"
|
||||
expand="block"
|
||||
className="ion-margin-top"
|
||||
>
|
||||
Play Again!
|
||||
</IonButton>
|
||||
</IonCardContent>
|
||||
</IonCard>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
</IonGrid>
|
||||
);
|
||||
};
|
@@ -0,0 +1,46 @@
|
||||
.difficultyContainer {
|
||||
|
||||
margin-top: -2rem !important;
|
||||
}
|
||||
|
||||
.startButton {
|
||||
|
||||
background-color: #994ec1;
|
||||
padding: 1.25rem;
|
||||
margin: 1rem;
|
||||
margin-top: -1rem;
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
color: white;
|
||||
border: 2px solid #632485;
|
||||
}
|
||||
|
||||
.questionTitle {
|
||||
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.answerButton {
|
||||
|
||||
height: fit-content;
|
||||
--padding-top: 1rem;
|
||||
--padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.mainGrid {
|
||||
|
||||
// margin-top: -2rem;
|
||||
}
|
||||
|
||||
.mainRow {
|
||||
|
||||
margin-top: -2rem;
|
||||
}
|
||||
|
||||
.emoji {
|
||||
|
||||
font-size: 4rem;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
padding-top: 1rem;
|
||||
}
|
@@ -0,0 +1,25 @@
|
||||
import { IonCard, IonCardContent, IonCardSubtitle, IonCol, IonItem, IonLabel, IonNote, IonRow } from "@ionic/react";
|
||||
|
||||
export const QuizStats = ({ chosenCategory, chosenDifficulty, currentQuestion, questionsLength, score }) => (
|
||||
|
||||
<IonRow>
|
||||
<IonCol size="12">
|
||||
<IonCard>
|
||||
<IonCardContent className="ion-text-center">
|
||||
<IonCardSubtitle>{ chosenCategory } | { chosenDifficulty }</IonCardSubtitle>
|
||||
<IonItem lines="none">
|
||||
<IonLabel className="ion-text-center">
|
||||
<IonCardSubtitle>Question</IonCardSubtitle>
|
||||
<IonNote>{ currentQuestion } / { questionsLength }</IonNote>
|
||||
</IonLabel>
|
||||
|
||||
<IonLabel className="ion-text-center">
|
||||
<IonCardSubtitle>Score</IonCardSubtitle>
|
||||
<IonNote>{ score }</IonNote>
|
||||
</IonLabel>
|
||||
</IonItem>
|
||||
</IonCardContent>
|
||||
</IonCard>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
);
|
@@ -0,0 +1,17 @@
|
||||
import { IonCol } from "@ionic/react";
|
||||
import { updateChosenCategory, updateChosenDifficulty } from "../store/SettingsStore";
|
||||
import styles from "./Settings.module.scss";
|
||||
|
||||
export const Category = ({ label, value, set, chosen }) => (
|
||||
|
||||
<IonCol id={ `categoryButton_${ value }` } size="6" className={ `${styles.category} ${chosen && styles.chosen} animate__animated` } onClick={ () => updateChosenCategory(value) }>
|
||||
<p>{ label }</p>
|
||||
</IonCol>
|
||||
);
|
||||
|
||||
export const Difficulty = ({ label, value, set, chosen }) => (
|
||||
|
||||
<IonCol id={ `difficultyButton_${ value }` } size="4" className={ `${ styles.category } ${ chosen && styles.chosen } animate__animated` } onClick={ () => updateChosenDifficulty(value) }>
|
||||
<p>{ label }</p>
|
||||
</IonCol>
|
||||
);
|
@@ -0,0 +1,20 @@
|
||||
.category {
|
||||
|
||||
height: 4rem;
|
||||
border: 5px solid rgb(255, 255, 255);
|
||||
background-color: #994ec1;
|
||||
color: white;
|
||||
border-radius: 10px;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
align-content: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.chosen {
|
||||
|
||||
border: 2px solid #3a1d49;
|
||||
font-weight: 700;
|
||||
}
|
33
03_source/mobile/src/pages/DemoQuizApp/index.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { IonRouterOutlet, IonTabs } from '@ionic/react';
|
||||
|
||||
import { Route, Redirect } from 'react-router';
|
||||
|
||||
import Home from './AppPages/Home';
|
||||
import Quiz from './AppPages/Quiz';
|
||||
import Questions from './AppPages/Questions';
|
||||
|
||||
import './style.scss';
|
||||
|
||||
function DemoQuizApp() {
|
||||
return (
|
||||
<IonTabs className="demo-quiz-app">
|
||||
<IonRouterOutlet>
|
||||
<Route exact path="/demo-quiz-app/home">
|
||||
<Home />
|
||||
</Route>
|
||||
|
||||
<Route exact path="/demo-quiz-app/quiz">
|
||||
<Quiz />
|
||||
</Route>
|
||||
|
||||
<Route exact path="/demo-quiz-app/questions">
|
||||
<Questions />
|
||||
</Route>
|
||||
|
||||
<Redirect exact path="/demo-quiz-app" to="/demo-quiz-app/home" />
|
||||
</IonRouterOutlet>
|
||||
</IonTabs>
|
||||
);
|
||||
}
|
||||
|
||||
export default DemoQuizApp;
|
11
03_source/mobile/src/pages/DemoQuizApp/questions/index.js
Normal file
@@ -0,0 +1,11 @@
|
||||
const API_URL = 'https://quizapi.io/api/v1/questions';
|
||||
const API_KEY = 'B27jnk1wmfEOQ42FtmrgBogiNTLLhOArJj29y24a';
|
||||
|
||||
export const fetchQuestions = async (category, difficulty) => {
|
||||
const response = await fetch(
|
||||
`${API_URL}?apiKey=${API_KEY}&category=${category}&difficulty=${difficulty}&limit=10`
|
||||
);
|
||||
const questions = await response.json();
|
||||
|
||||
return questions;
|
||||
};
|
10
03_source/mobile/src/pages/DemoQuizApp/store/Selectors.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import { createSelector } from "reselect";
|
||||
|
||||
const getState = state => state;
|
||||
|
||||
// Getters
|
||||
export const getCategories = createSelector(getState, state => state.categories);
|
||||
export const getDifficulties = createSelector(getState, state => state.difficulties);
|
||||
|
||||
export const getChosenCategory = createSelector(getState, state => state.chosenCategory);
|
||||
export const getChosenDifficulty = createSelector(getState, state => state.chosenDifficulty);
|
@@ -0,0 +1,60 @@
|
||||
import { Store } from "pullstate";
|
||||
|
||||
const SettingsStore = new Store({
|
||||
|
||||
categories: [
|
||||
{
|
||||
label: "Code",
|
||||
value: "code",
|
||||
},
|
||||
{
|
||||
label: "Linux",
|
||||
value: "linux"
|
||||
},
|
||||
{
|
||||
label: "Dev Ops",
|
||||
value: "devops"
|
||||
},
|
||||
{
|
||||
label: "Authentication",
|
||||
value: "authentication"
|
||||
},
|
||||
{
|
||||
label: "Bash",
|
||||
value: "bash"
|
||||
},
|
||||
{
|
||||
label: "SQL",
|
||||
value: "sql"
|
||||
}
|
||||
],
|
||||
difficulties: [
|
||||
{
|
||||
label: "Easy",
|
||||
value: "easy"
|
||||
},
|
||||
{
|
||||
label: "Medium",
|
||||
value: "medium"
|
||||
},
|
||||
{
|
||||
label: "Hard",
|
||||
value: "hard"
|
||||
}
|
||||
],
|
||||
|
||||
chosenCategory: false,
|
||||
chosenDifficulty: false
|
||||
});
|
||||
|
||||
export default SettingsStore;
|
||||
|
||||
export const updateChosenCategory = category => {
|
||||
|
||||
SettingsStore.update(s => { s.chosenCategory = category });
|
||||
}
|
||||
|
||||
export const updateChosenDifficulty = difficulty => {
|
||||
|
||||
SettingsStore.update(s => { s.chosenDifficulty = difficulty });
|
||||
}
|
1
03_source/mobile/src/pages/DemoQuizApp/store/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default as SettingsStore } from "./SettingsStore";
|
113
03_source/mobile/src/pages/DemoQuizApp/style.scss
Normal file
@@ -0,0 +1,65 @@
|
||||
import {
|
||||
IonButton,
|
||||
IonButtons,
|
||||
IonContent,
|
||||
IonHeader,
|
||||
IonMenuButton,
|
||||
IonPage,
|
||||
IonTitle,
|
||||
IonToolbar,
|
||||
useIonActionSheet,
|
||||
} from '@ionic/react';
|
||||
|
||||
const ActionSheet = () => {
|
||||
const [present, dismiss] = useIonActionSheet();
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonButtons slot="start">
|
||||
<IonMenuButton />
|
||||
</IonButtons>
|
||||
<IonTitle>Action Sheet</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonContent fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large">Action Sheet</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonButton
|
||||
expand="block"
|
||||
onClick={() =>
|
||||
present({
|
||||
buttons: [{ text: 'Ok' }, { text: 'Cancel' }],
|
||||
header: 'Action Sheet',
|
||||
})
|
||||
}
|
||||
>
|
||||
Show ActionSheet
|
||||
</IonButton>
|
||||
<IonButton
|
||||
expand="block"
|
||||
onClick={() => present([{ text: 'Ok' }, { text: 'Cancel' }], 'Action Sheet')}
|
||||
>
|
||||
Show ActionSheet using params
|
||||
</IonButton>
|
||||
<IonButton
|
||||
expand="block"
|
||||
onClick={() => {
|
||||
present([{ text: 'Ok' }, { text: 'Cancel' }], 'Action Sheet');
|
||||
setTimeout(dismiss, 3000);
|
||||
}}
|
||||
>
|
||||
Show ActionSheet, hide after 3 seconds
|
||||
</IonButton>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActionSheet;
|
@@ -0,0 +1,56 @@
|
||||
import {
|
||||
IonButton,
|
||||
IonButtons,
|
||||
IonContent,
|
||||
IonHeader,
|
||||
IonMenuButton,
|
||||
IonPage,
|
||||
IonTitle,
|
||||
IonToolbar,
|
||||
useIonAlert,
|
||||
} from '@ionic/react';
|
||||
|
||||
const Alert = () => {
|
||||
const [present] = useIonAlert();
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonButtons slot="start">
|
||||
<IonMenuButton />
|
||||
</IonButtons>
|
||||
<IonTitle>Alert</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonContent fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large">Alert</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonButton
|
||||
expand="block"
|
||||
onClick={() =>
|
||||
present({
|
||||
cssClass: 'my-css',
|
||||
header: 'Alert',
|
||||
message: 'alert from hook',
|
||||
buttons: ['Cancel', { text: 'Ok', handler: (d) => console.log('ok pressed') }],
|
||||
onDidDismiss: (e) => console.log('did dismiss'),
|
||||
})
|
||||
}
|
||||
>
|
||||
Show Alert
|
||||
</IonButton>
|
||||
<IonButton expand="block" onClick={() => present('hello with params', [{ text: 'Ok' }])}>
|
||||
Show Alert using params
|
||||
</IonButton>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Alert;
|
@@ -0,0 +1,76 @@
|
||||
import {
|
||||
IonButtons,
|
||||
IonCard,
|
||||
IonCardHeader,
|
||||
IonContent,
|
||||
IonHeader,
|
||||
IonMenuButton,
|
||||
IonPage,
|
||||
IonTitle,
|
||||
IonToolbar,
|
||||
IonCardTitle,
|
||||
IonCardSubtitle,
|
||||
IonCardContent,
|
||||
IonText,
|
||||
} from '@ionic/react';
|
||||
|
||||
const All = () => {
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonButtons slot="start">
|
||||
<IonMenuButton />
|
||||
</IonButtons>
|
||||
<IonTitle>All</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonContent fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large">All</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonCard>
|
||||
<IonCardHeader>
|
||||
<IonCardSubtitle>Sample usage</IonCardSubtitle>
|
||||
<IonCardTitle>Overlay Hooks</IonCardTitle>
|
||||
</IonCardHeader>
|
||||
|
||||
<IonCardContent>
|
||||
<IonText>
|
||||
<p>
|
||||
In Ionic React 5.6, the team packaged up a new set of hooks for controlling overlay
|
||||
components that they thought we might like. What is an overlay you ask? It’s the
|
||||
term that Ionic give components that display over your current content, such as
|
||||
alerts, modals, toasts, etc.
|
||||
</p>
|
||||
</IonText>
|
||||
<br />
|
||||
<IonText>
|
||||
<p>
|
||||
All of the code is taken from the Ionic Framework docs. You can find the blog post
|
||||
outlining these new overlay hooks{' '}
|
||||
<a
|
||||
href="https://ionicframework.com/blog/introducing-the-new-overlay-hooks-for-ionic-react/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
here.
|
||||
</a>
|
||||
</p>
|
||||
</IonText>
|
||||
<br />
|
||||
<IonText>
|
||||
<p>Check out the samples by navigating to a respective one in the side menu.</p>
|
||||
</IonText>
|
||||
</IonCardContent>
|
||||
</IonCard>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default All;
|
@@ -0,0 +1,52 @@
|
||||
import {
|
||||
IonButton,
|
||||
IonButtons,
|
||||
IonContent,
|
||||
IonHeader,
|
||||
IonMenuButton,
|
||||
IonPage,
|
||||
IonTitle,
|
||||
IonToolbar,
|
||||
useIonLoading,
|
||||
} from '@ionic/react';
|
||||
|
||||
const Loading = () => {
|
||||
const [present] = useIonLoading();
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonButtons slot="start">
|
||||
<IonMenuButton />
|
||||
</IonButtons>
|
||||
<IonTitle>Loading</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonContent fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large">Loading</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonButton
|
||||
expand="block"
|
||||
onClick={() =>
|
||||
present({
|
||||
duration: 3000,
|
||||
})
|
||||
}
|
||||
>
|
||||
Show Loading
|
||||
</IonButton>
|
||||
<IonButton expand="block" onClick={() => present('Loading', 2000, 'dots')}>
|
||||
Show Loading using params
|
||||
</IonButton>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Loading;
|
@@ -0,0 +1,80 @@
|
||||
import {
|
||||
IonButton,
|
||||
IonButtons,
|
||||
IonContent,
|
||||
IonHeader,
|
||||
IonMenuButton,
|
||||
IonPage,
|
||||
IonText,
|
||||
IonTitle,
|
||||
IonToolbar,
|
||||
useIonModal,
|
||||
} from '@ionic/react';
|
||||
import { useState } from 'react';
|
||||
|
||||
const Modal = () => {
|
||||
const Body = ({ count, onDismiss, onIncrement }) => (
|
||||
<div className="ion-text-center">
|
||||
<IonText color="dark" className="ion-text-center">
|
||||
Count: {count}
|
||||
</IonText>
|
||||
<IonButton expand="block" onClick={() => onIncrement()}>
|
||||
Increment Count
|
||||
</IonButton>
|
||||
<IonButton expand="block" onClick={() => onDismiss()}>
|
||||
Close
|
||||
</IonButton>
|
||||
</div>
|
||||
);
|
||||
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
const handleIncrement = () => {
|
||||
setCount(count + 1);
|
||||
};
|
||||
|
||||
const handleDismiss = () => {
|
||||
dismiss();
|
||||
};
|
||||
|
||||
const [present, dismiss] = useIonModal(Body, {
|
||||
count,
|
||||
onDismiss: handleDismiss,
|
||||
onIncrement: handleIncrement,
|
||||
});
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonButtons slot="start">
|
||||
<IonMenuButton />
|
||||
</IonButtons>
|
||||
<IonTitle>Modal</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonContent fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large">Modal</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonButton
|
||||
expand="block"
|
||||
onClick={() => {
|
||||
present({
|
||||
cssClass: 'my-class',
|
||||
});
|
||||
}}
|
||||
>
|
||||
Show Modal
|
||||
</IonButton>
|
||||
<div>Count: {count}</div>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Modal;
|
@@ -0,0 +1,104 @@
|
||||
import {
|
||||
IonButton,
|
||||
IonButtons,
|
||||
IonContent,
|
||||
IonHeader,
|
||||
IonMenuButton,
|
||||
IonPage,
|
||||
IonTitle,
|
||||
IonToolbar,
|
||||
useIonPicker,
|
||||
} from '@ionic/react';
|
||||
import { useState } from 'react';
|
||||
|
||||
const Picker = () => {
|
||||
const [present] = useIonPicker();
|
||||
const [value, setValue] = useState('');
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonButtons slot="start">
|
||||
<IonMenuButton />
|
||||
</IonButtons>
|
||||
<IonTitle>Picker</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonContent fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large">Picker</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonButton
|
||||
expand="block"
|
||||
onClick={() =>
|
||||
present({
|
||||
buttons: [
|
||||
{
|
||||
text: 'Confirm',
|
||||
handler: (selected) => {
|
||||
setValue(selected.animal.value);
|
||||
},
|
||||
},
|
||||
],
|
||||
columns: [
|
||||
{
|
||||
name: 'animal',
|
||||
options: [
|
||||
{ text: 'Dog', value: 'dog' },
|
||||
{ text: 'Cat', value: 'cat' },
|
||||
{ text: 'Bird', value: 'bird' },
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
>
|
||||
Show Picker
|
||||
</IonButton>
|
||||
<IonButton
|
||||
expand="block"
|
||||
onClick={() =>
|
||||
present(
|
||||
[
|
||||
{
|
||||
name: 'animal',
|
||||
options: [
|
||||
{ text: 'Dog', value: 'dog' },
|
||||
{ text: 'Cat', value: 'cat' },
|
||||
{ text: 'Bird', value: 'bird' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'vehicle',
|
||||
options: [
|
||||
{ text: 'Car', value: 'car' },
|
||||
{ text: 'Truck', value: 'truck' },
|
||||
{ text: 'Bike', value: 'bike' },
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
text: 'Confirm',
|
||||
handler: (selected) => {
|
||||
setValue(`${selected.animal.value}, ${selected.vehicle.value}`);
|
||||
},
|
||||
},
|
||||
]
|
||||
)
|
||||
}
|
||||
>
|
||||
Show Picker using params
|
||||
</IonButton>
|
||||
{value && <div>Selected Value: {value}</div>}
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Picker;
|
@@ -0,0 +1,65 @@
|
||||
import {
|
||||
IonButtons,
|
||||
IonContent,
|
||||
IonHeader,
|
||||
IonItem,
|
||||
IonListHeader,
|
||||
IonMenuButton,
|
||||
IonPage,
|
||||
IonTitle,
|
||||
IonToolbar,
|
||||
IonList,
|
||||
useIonPopover,
|
||||
IonButton,
|
||||
} from '@ionic/react';
|
||||
|
||||
const Popover = () => {
|
||||
const PopoverList = ({ onHide }) => (
|
||||
<IonList>
|
||||
<IonListHeader>Ionic</IonListHeader>
|
||||
<IonItem button>Learn Ionic</IonItem>
|
||||
<IonItem button>Documentation</IonItem>
|
||||
<IonItem button>Showcase</IonItem>
|
||||
<IonItem button>GitHub Repo</IonItem>
|
||||
<IonItem lines="none" detail={false} button onClick={onHide}>
|
||||
Close
|
||||
</IonItem>
|
||||
</IonList>
|
||||
);
|
||||
|
||||
const [present, dismiss] = useIonPopover(PopoverList, { onHide: () => dismiss() });
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonButtons slot="start">
|
||||
<IonMenuButton />
|
||||
</IonButtons>
|
||||
<IonTitle>Popover</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonContent fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large">Popover</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonButton
|
||||
expand="block"
|
||||
onClick={(e) =>
|
||||
present({
|
||||
event: e.nativeEvent,
|
||||
})
|
||||
}
|
||||
>
|
||||
Show Popover
|
||||
</IonButton>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Popover;
|
@@ -0,0 +1,58 @@
|
||||
import {
|
||||
IonButton,
|
||||
IonButtons,
|
||||
IonContent,
|
||||
IonHeader,
|
||||
IonMenuButton,
|
||||
IonPage,
|
||||
IonTitle,
|
||||
IonToolbar,
|
||||
useIonToast,
|
||||
} from '@ionic/react';
|
||||
|
||||
const Toast = () => {
|
||||
const [present, dismiss] = useIonToast();
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonButtons slot="start">
|
||||
<IonMenuButton />
|
||||
</IonButtons>
|
||||
<IonTitle>Toast</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonContent fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large">Toast</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonButton
|
||||
expand="block"
|
||||
onClick={() =>
|
||||
present({
|
||||
buttons: [{ text: 'hide', handler: () => dismiss() }],
|
||||
message: 'toast from hook, click hide to dismiss',
|
||||
onDidDismiss: () => console.log('dismissed'),
|
||||
onWillDismiss: () => console.log('will dismiss'),
|
||||
})
|
||||
}
|
||||
>
|
||||
Show Toast
|
||||
</IonButton>
|
||||
<IonButton expand="block" onClick={() => present('hello from hook', 3000)}>
|
||||
Show Toast using params, closes in 3 secs
|
||||
</IonButton>
|
||||
<IonButton expand="block" onClick={dismiss}>
|
||||
Hide Toast
|
||||
</IonButton>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Toast;
|
@@ -0,0 +1,113 @@
|
||||
ion-menu ion-content {
|
||||
--background: var(--ion-item-background, var(--ion-background-color, #fff));
|
||||
}
|
||||
|
||||
ion-menu.md ion-content {
|
||||
--padding-start: 8px;
|
||||
--padding-end: 8px;
|
||||
--padding-top: 20px;
|
||||
--padding-bottom: 20px;
|
||||
}
|
||||
|
||||
ion-menu.md ion-list {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
ion-menu.md ion-note {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
ion-menu.md ion-list-header, ion-menu.md ion-note {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
ion-menu.md ion-list#inbox-list {
|
||||
border-bottom: 1px solid var(--ion-color-step-150, #d7d8da);
|
||||
}
|
||||
|
||||
ion-menu.md ion-list#inbox-list ion-list-header {
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
ion-menu.md ion-list#labels-list ion-list-header {
|
||||
font-size: 16px;
|
||||
margin-bottom: 18px;
|
||||
color: #757575;
|
||||
min-height: 26px;
|
||||
}
|
||||
|
||||
ion-menu.md ion-item {
|
||||
--padding-start: 10px;
|
||||
--padding-end: 10px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
ion-menu.md ion-item.selected {
|
||||
--background: rgba(var(--ion-color-primary-rgb), 0.14);
|
||||
}
|
||||
|
||||
ion-menu.md ion-item.selected ion-icon {
|
||||
color: var(--ion-color-primary);
|
||||
}
|
||||
|
||||
ion-menu.md ion-item ion-icon {
|
||||
color: #616e7e;
|
||||
}
|
||||
|
||||
ion-menu.md ion-item ion-label {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
ion-menu.ios ion-content {
|
||||
--padding-bottom: 20px;
|
||||
}
|
||||
|
||||
ion-menu.ios ion-list {
|
||||
padding: 20px 0 0 0;
|
||||
}
|
||||
|
||||
ion-menu.ios ion-note {
|
||||
line-height: 24px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
ion-menu.ios ion-item {
|
||||
--padding-start: 16px;
|
||||
--padding-end: 16px;
|
||||
--min-height: 50px;
|
||||
}
|
||||
|
||||
ion-menu.ios ion-item ion-icon {
|
||||
font-size: 24px;
|
||||
color: #73849a;
|
||||
}
|
||||
|
||||
ion-menu.ios ion-item .selected ion-icon {
|
||||
color: var(--ion-color-primary);
|
||||
}
|
||||
|
||||
ion-menu.ios ion-list#labels-list ion-list-header {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
ion-menu.ios ion-list-header,
|
||||
ion-menu.ios ion-note {
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
ion-menu.ios ion-note {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
ion-note {
|
||||
display: inline-block;
|
||||
font-size: 16px;
|
||||
color: var(--ion-color-medium-shade);
|
||||
}
|
||||
|
||||
ion-item.selected {
|
||||
--color: var(--ion-color-primary);
|
||||
}
|
@@ -0,0 +1,37 @@
|
||||
import { IonContent, IonIcon, IonItem, IonLabel, IonList, IonListHeader, IonMenu, IonMenuToggle, IonNote } from '@ionic/react';
|
||||
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { star, starOutline } from 'ionicons/icons';
|
||||
import './Menu.css';
|
||||
|
||||
const Menu = ({ pages }) => {
|
||||
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<IonMenu contentId="main" type="overlay">
|
||||
<IonContent>
|
||||
<IonList id="inbox-list">
|
||||
<IonListHeader>Overlay Hooks</IonListHeader>
|
||||
<IonNote>Choose one below to see a demo</IonNote>
|
||||
|
||||
{ pages.map((appPage, index) => {
|
||||
|
||||
const isSelected = location.pathname === appPage.url;
|
||||
|
||||
return (
|
||||
<IonMenuToggle key={ index } autoHide={false}>
|
||||
<IonItem className={ isSelected ? 'selected' : '' } routerLink={ appPage.url } routerDirection="none" lines="none" detail={false}>
|
||||
<IonIcon slot="start" icon={ isSelected ? star : starOutline } />
|
||||
<IonLabel>{ appPage.label }</IonLabel>
|
||||
</IonItem>
|
||||
</IonMenuToggle>
|
||||
);
|
||||
})}
|
||||
</IonList>
|
||||
</IonContent>
|
||||
</IonMenu>
|
||||
);
|
||||
};
|
||||
|
||||
export default Menu;
|
62
03_source/mobile/src/pages/DemoReactOverlayHooks/index.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import {
|
||||
IonIcon,
|
||||
IonLabel,
|
||||
IonRouterOutlet,
|
||||
IonSplitPane,
|
||||
IonTabBar,
|
||||
IonTabButton,
|
||||
IonTabs,
|
||||
} from '@ionic/react';
|
||||
|
||||
import { cloudOutline, searchOutline } from 'ionicons/icons';
|
||||
import { Route, Redirect } from 'react-router';
|
||||
import Menu from './components/Menu';
|
||||
|
||||
import All from './AppPages/All';
|
||||
import ActionSheet from './AppPages/ActionSheet';
|
||||
import Alert from './AppPages/Alert';
|
||||
import Loading from './AppPages/Loading';
|
||||
import Modal from './AppPages/Modal';
|
||||
import Picker from './AppPages/Picker';
|
||||
import Popover from './AppPages/Popover';
|
||||
import Toast from './AppPages/Toast';
|
||||
import './style.scss';
|
||||
|
||||
function DemoReactOverlayHooks() {
|
||||
const pages = [
|
||||
{ label: 'All', url: '/overlay/all', component: All },
|
||||
{ label: 'Action Sheet', url: '/overlay/action-sheet', component: ActionSheet },
|
||||
{ label: 'Alert', url: '/overlay/alert', component: Alert },
|
||||
{ label: 'Loading', url: '/overlay/loading', component: Loading },
|
||||
{ label: 'Modal', url: '/overlay/modal', component: Modal },
|
||||
{ label: 'Picker', url: '/overlay/picker', component: Picker },
|
||||
{ label: 'Popover', url: '/overlay/popover', component: Popover },
|
||||
{ label: 'Toast', url: '/overlay/toast', component: Toast },
|
||||
];
|
||||
|
||||
return (
|
||||
<IonSplitPane contentId="main">
|
||||
<Menu pages={pages} />
|
||||
<IonRouterOutlet id="main">
|
||||
<Route path="/demo-react-overlay-hooks" exact={true}>
|
||||
<Redirect to="/demo-react-overlay-hooks/overlay/all" />
|
||||
</Route>
|
||||
|
||||
{pages.map((page, index) => {
|
||||
const pageComponent = page.component;
|
||||
|
||||
return (
|
||||
<Route
|
||||
key={index}
|
||||
path={`/demo-react-overlay-hooks${page.url}`}
|
||||
exact={true}
|
||||
component={pageComponent}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</IonRouterOutlet>
|
||||
</IonSplitPane>
|
||||
);
|
||||
}
|
||||
|
||||
export default DemoReactOverlayHooks;
|
103
03_source/mobile/src/pages/DemoReactOverlayHooks/style.scss
Normal file
@@ -0,0 +1,103 @@
|
||||
#about-page {
|
||||
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: 30%;
|
||||
}
|
||||
|
||||
.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('/assets/WeatherDemo/img/about/madison.jpg');
|
||||
}
|
||||
|
||||
.about-header .austin {
|
||||
background-image: url('/assets/WeatherDemo/img/about/austin.jpg');
|
||||
}
|
||||
|
||||
.about-header .chicago {
|
||||
background-image: url('/assets/WeatherDemo/img/about/chicago.jpg');
|
||||
}
|
||||
|
||||
.about-header .seattle {
|
||||
background-image: url('/assets/WeatherDemo/img/about/seattle.jpg');
|
||||
}
|
||||
|
||||
.about-info {
|
||||
position: relative;
|
||||
margin-top: -10px;
|
||||
border-radius: 10px;
|
||||
background: var(--ion-background-color, #fff);
|
||||
z-index: 2; // display rounded border above header image
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
#date-input-popover {
|
||||
--offset-y: -var(--ion-safe-area-bottom);
|
||||
|
||||
--max-width: 90%;
|
||||
--width: 336px;
|
||||
}
|
198
03_source/mobile/src/pages/DemoReactPollApp/AppPages/Add.jsx
Normal file
@@ -0,0 +1,198 @@
|
||||
import { IonBackButton, IonButton, IonButtons, IonCardTitle, IonCol, IonContent, IonFooter, IonGrid, IonHeader, IonIcon, IonItem, IonLabel, IonMenuButton, IonPage, IonRow, IonTextarea, IonTitle, IonToolbar, useIonRouter, useIonViewDidEnter } from '@ionic/react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { GithubPicker } from 'react-color';
|
||||
import { getColors } from '../helpers/utils';
|
||||
import { addOutline } from 'ionicons/icons';
|
||||
import { PollDuration } from '../components/PollDuration';
|
||||
import { PollAnswer } from '../components/PollAnswer';
|
||||
import { addPoll } from '../store/PollStore';
|
||||
|
||||
const Add = () => {
|
||||
|
||||
const router = useIonRouter();
|
||||
const [ showPicker, setShowPicker ] = useState(false);
|
||||
const [ pollQuestion, setPollQuestion ] = useState("");
|
||||
const [ pollColor, setPollColor ] = useState("#427ed8");
|
||||
const [ pollAnswers, setPollAnswers ] = useState([]);
|
||||
|
||||
const [ pollDays, setPollDays ] = useState(0);
|
||||
const [ pollHours, setPollHours ] = useState(0);
|
||||
const [ pollMins, setPollMins ] = useState(0);
|
||||
|
||||
const pickerColors = [
|
||||
|
||||
"#759dc7",
|
||||
"#68bd8d",
|
||||
"#bd7368",
|
||||
"#8d68bd",
|
||||
"#bd68ac",
|
||||
"#6868bd",
|
||||
"#68a8bd",
|
||||
"#68bda5",
|
||||
"#bd9868",
|
||||
"#d84848",
|
||||
"#d87c48",
|
||||
"#d8bb48",
|
||||
"#7c7c7c"
|
||||
];
|
||||
|
||||
const colors = pollColor && getColors(pollColor);
|
||||
|
||||
useIonViewDidEnter(() => {
|
||||
|
||||
setShowPicker(false);
|
||||
setPollQuestion("");
|
||||
setPollColor("#427ed8");
|
||||
setPollAnswers([]);
|
||||
|
||||
setPollDays(0);
|
||||
setPollHours(0);
|
||||
setPollMins(0);
|
||||
});
|
||||
|
||||
const handleAdd = async () => {
|
||||
|
||||
const timeLeftDays = pollDays !== 0 && pollDays !== "" ? `${ pollDays } days, ` : "";
|
||||
const timeLeftHours = pollHours !== 0 && pollHours !== "" ? `${ pollHours } hours, ` : "";
|
||||
const timeLeftMins = pollMins !== 0 && pollMins !== "" ? `${ pollMins } mins` : "";
|
||||
|
||||
const timeLeft = `${ timeLeftDays }${ timeLeftHours }${ timeLeftMins }`;
|
||||
|
||||
const poll = {
|
||||
|
||||
id: Date.now(),
|
||||
question: pollQuestion,
|
||||
color: pollColor,
|
||||
timeLeft,
|
||||
answers: pollAnswers,
|
||||
totalVotes: 0,
|
||||
voted: false
|
||||
};
|
||||
|
||||
addPoll(poll);
|
||||
router.push("/page/view");
|
||||
}
|
||||
|
||||
const addAnswer = () => {
|
||||
|
||||
const answer = {
|
||||
|
||||
id: Date.now(),
|
||||
answer: "",
|
||||
votes: 0,
|
||||
voted: false,
|
||||
percent: 0
|
||||
};
|
||||
|
||||
setPollAnswers(prev => [...prev, answer ] );
|
||||
}
|
||||
|
||||
const removeAnswer = answer => {
|
||||
|
||||
const newAnswers = pollAnswers.filter((p) => p !== answer);
|
||||
setPollAnswers(newAnswers);
|
||||
}
|
||||
|
||||
const handleChange = (e, index) => {
|
||||
|
||||
const newAnswers = [ ...pollAnswers ];
|
||||
newAnswers[index].answer = e.target.value;
|
||||
setPollAnswers(newAnswers);
|
||||
}
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonButtons slot="start">
|
||||
<IonBackButton text="Ionic Polls" style={{ color: pollColor ? pollColor : "" }} />
|
||||
</IonButtons>
|
||||
<IonTitle>Add Poll</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonContent fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large">Add Poll</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonGrid className="animate__animated animate__fadeIn">
|
||||
<IonRow>
|
||||
<IonCol size="12">
|
||||
<IonButton expand="block" onClick={ () => setShowPicker(!showPicker) } size="large" fill="solid" style={ colors.votedButtonStyle }>Poll Color</IonButton>
|
||||
{ showPicker && <GithubPicker colors={ pickerColors } color={ pollColor } onChange={ color => setPollColor(color.hex) } onChangeComplete={ () => setShowPicker(false) } /> }
|
||||
</IonCol>
|
||||
<IonCol size="12">
|
||||
<IonItem lines="full">
|
||||
<IonLabel position="floating">Poll Question</IonLabel>
|
||||
<IonTextarea rows="2" value={ pollQuestion } onIonChange={ e => setPollQuestion(e.target.value) } placeholder="A question to ask..." />
|
||||
</IonItem>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
|
||||
<IonRow className="ion-margin-top">
|
||||
<IonCol size="12" className="ion-padding-start">
|
||||
<IonCardTitle>Poll Duration</IonCardTitle>
|
||||
</IonCol>
|
||||
|
||||
<IonCol size="12">
|
||||
<IonRow className="ion-justify-content-center ion-align-items-center ion-text-center">
|
||||
<PollDuration label="Days" value={ pollDays } setter={ setPollDays } />
|
||||
<PollDuration label="Hours" value={ pollHours } setter={ setPollHours } />
|
||||
<PollDuration label="Mins" value={ pollMins } setter={ setPollMins } />
|
||||
</IonRow>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
|
||||
<IonRow className="ion-margin-top ion-align-items-center">
|
||||
<IonCol size="10" className="ion-padding-start">
|
||||
<IonCardTitle className="ion-justify-content-between">
|
||||
Poll Answers
|
||||
</IonCardTitle>
|
||||
</IonCol>
|
||||
|
||||
<IonCol size="2">
|
||||
<IonButton onClick={ addAnswer } disabled={ !pollAnswers.length > 0 }>
|
||||
<IonIcon icon={ addOutline } />
|
||||
</IonButton>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
|
||||
{ pollAnswers.length > 0 && pollAnswers.map((answer, index) => {
|
||||
|
||||
return <PollAnswer key={ `pollAnswer_${ index }` } index={ index } value={ answer } remove={ removeAnswer } change={ handleChange } />;
|
||||
})}
|
||||
|
||||
<IonRow>
|
||||
{ !pollAnswers.length &&
|
||||
|
||||
<IonCol size="12">
|
||||
<IonItem lines="full" className="ion-justify-content-center ion-align-items-center">
|
||||
<IonLabel className="ion-text-center">
|
||||
<p>There are currenty no answers added for this poll.</p>
|
||||
<IonButton color="success" onClick={ addAnswer }>Add one now</IonButton>
|
||||
</IonLabel>
|
||||
</IonItem>
|
||||
</IonCol>
|
||||
}
|
||||
</IonRow>
|
||||
</IonGrid>
|
||||
</IonContent>
|
||||
|
||||
<IonFooter className="ion-padding-bottom">
|
||||
<IonRow className="ion-padding-start ion-padding-end ion-padding-bottom ion-padding-top">
|
||||
<IonCol size="12">
|
||||
<IonButton fill="outline" expand="block" onClick={ handleAdd }>
|
||||
Save
|
||||
</IonButton>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
</IonFooter>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Add;
|
@@ -0,0 +1,96 @@
|
||||
import {
|
||||
IonButton,
|
||||
IonButtons,
|
||||
IonCol,
|
||||
IonContent,
|
||||
IonHeader,
|
||||
IonIcon,
|
||||
IonPage,
|
||||
IonRow,
|
||||
IonTitle,
|
||||
IonToolbar,
|
||||
useIonRouter,
|
||||
} from '@ionic/react';
|
||||
|
||||
import { Geolocation } from '@capacitor/geolocation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { SkeletonDashboard } from '../components/SkeletonDashboard';
|
||||
import { chevronBackOutline, refreshOutline } from 'ionicons/icons';
|
||||
import { CurrentWeather } from '../components/CurrentWeather';
|
||||
|
||||
function Tab1() {
|
||||
const router = useIonRouter();
|
||||
|
||||
const [currentWeather, setCurrentWeather] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
getCurrentPosition();
|
||||
}, []);
|
||||
|
||||
const getCurrentPosition = async () => {
|
||||
setCurrentWeather(false);
|
||||
const coordinates = await Geolocation.getCurrentPosition();
|
||||
getAddress(coordinates.coords);
|
||||
};
|
||||
|
||||
const getAddress = async (coords) => {
|
||||
const query = `${coords.latitude},${coords.longitude}`;
|
||||
const response = await fetch(
|
||||
`https://api.weatherapi.com/v1/current.json?key=f93eb660b2424258bf5155016210712&q=${query}`
|
||||
);
|
||||
|
||||
const data = await response.json();
|
||||
console.log(data);
|
||||
setCurrentWeather(data);
|
||||
};
|
||||
|
||||
// const router = useIonRouter();
|
||||
function handleBackClick() {
|
||||
router.goBack();
|
||||
}
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonTitle>My Weather</IonTitle>
|
||||
|
||||
<IonButtons slot="end">
|
||||
<IonButton onClick={() => getCurrentPosition()}>
|
||||
<IonIcon icon={refreshOutline} color="primary" />
|
||||
</IonButton>
|
||||
</IonButtons>
|
||||
|
||||
<IonButtons slot="start">
|
||||
<IonButton onClick={() => handleBackClick()}>
|
||||
<IonIcon icon={chevronBackOutline} color="primary" />
|
||||
</IonButton>
|
||||
</IonButtons>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<IonContent fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large">Dashboard</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonRow className="ion-margin-start ion-margin-end ion-justify-content-center ion-text-center">
|
||||
<IonCol size="12">
|
||||
<h4>Here's your location based weather</h4>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
|
||||
<div style={{ marginTop: '-1.5rem' }}>
|
||||
{currentWeather ? (
|
||||
<CurrentWeather currentWeather={currentWeather} />
|
||||
) : (
|
||||
<SkeletonDashboard />
|
||||
)}
|
||||
</div>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
}
|
||||
|
||||
export default Tab1;
|
@@ -0,0 +1,81 @@
|
||||
import {
|
||||
IonButton,
|
||||
IonCol,
|
||||
IonContent,
|
||||
IonHeader,
|
||||
IonPage,
|
||||
IonRow,
|
||||
IonSearchbar,
|
||||
IonTitle,
|
||||
IonToolbar,
|
||||
} from '@ionic/react';
|
||||
import { useState } from 'react';
|
||||
import { CurrentWeather } from '../components/CurrentWeather';
|
||||
|
||||
function Tab2() {
|
||||
const [search, setSearch] = useState('');
|
||||
const [currentWeather, setCurrentWeather] = useState(false);
|
||||
|
||||
const performSearch = async () => {
|
||||
getAddress(search);
|
||||
};
|
||||
|
||||
const getAddress = async (city) => {
|
||||
const response = await fetch(
|
||||
`https://api.weatherapi.com/v1/current.json?key=f93eb660b2424258bf5155016210712&q=${city}&aqi=no`
|
||||
);
|
||||
const data = await response.json();
|
||||
|
||||
if (data && data.current && data.location) {
|
||||
setCurrentWeather(data);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonTitle>Search</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<IonContent fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large">Search</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonRow className="ion-justify-content-center ion-margin-top ion-align-items-center">
|
||||
<IonCol size="7">
|
||||
<IonSearchbar
|
||||
placeholder="Try 'London'"
|
||||
animated
|
||||
value={search}
|
||||
onIonChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
</IonCol>
|
||||
|
||||
<IonCol size="5">
|
||||
<IonButton
|
||||
expand="block"
|
||||
className="ion-margin-start ion-margin-end"
|
||||
onClick={performSearch}
|
||||
>
|
||||
Search
|
||||
</IonButton>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
|
||||
<div style={{ marginTop: '-0.8rem' }}>
|
||||
{currentWeather ? (
|
||||
<CurrentWeather currentWeather={currentWeather} />
|
||||
) : (
|
||||
<h3 className="ion-text-center">Your search result will appear here</h3>
|
||||
)}
|
||||
</div>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
}
|
||||
|
||||
export default Tab2;
|
@@ -0,0 +1,91 @@
|
||||
import {
|
||||
IonButton,
|
||||
IonButtons,
|
||||
IonCard,
|
||||
IonCardHeader,
|
||||
IonCardSubtitle,
|
||||
IonCardTitle,
|
||||
IonCol,
|
||||
IonContent,
|
||||
IonFooter,
|
||||
IonHeader,
|
||||
IonIcon,
|
||||
IonMenuButton,
|
||||
IonPage,
|
||||
IonRow,
|
||||
IonTitle,
|
||||
IonToolbar,
|
||||
} from '@ionic/react';
|
||||
import { arrowForwardOutline } from 'ionicons/icons';
|
||||
import { getCardStyle } from '../helpers/utils';
|
||||
import { PollStore } from '../store';
|
||||
import { getPolls } from '../store/Selectors';
|
||||
import styles from './View.module.scss';
|
||||
|
||||
const View = () => {
|
||||
const polls = PollStore.useState(getPolls);
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonButtons slot="start">
|
||||
<IonMenuButton />
|
||||
</IonButtons>
|
||||
<IonTitle>Ionic Polls</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonContent fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large">Ionic Polls</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
{polls.map((poll) => {
|
||||
const colors = getCardStyle(poll.color);
|
||||
|
||||
return (
|
||||
<IonCard
|
||||
className={`${styles.pollQuestion} animate__animated animate__fadeIn`}
|
||||
style={{ backgroundColor: colors.backgroundColor }}
|
||||
routerLink={`/demo-react-poll-app/page/view/${poll.id}`}
|
||||
routerDirection="forward"
|
||||
>
|
||||
<IonRow className="ion-align-items-center">
|
||||
<IonCol size="9">
|
||||
<IonCardHeader>
|
||||
<IonCardTitle style={{ color: colors.textColor }}>{poll.question}</IonCardTitle>
|
||||
<IonCardSubtitle style={{ color: colors.subTextColor }}>{poll.timeLeft} left</IonCardSubtitle>
|
||||
<p style={{ color: colors.textColor }}>{poll.totalVotes} votes already</p>
|
||||
<p style={colors.statusBadge}>{poll.voted ? 'You have voted on this poll' : "You haven't voted on this poll"}</p>
|
||||
</IonCardHeader>
|
||||
</IonCol>
|
||||
|
||||
<IonCol size="3">
|
||||
<IonButton style={colors.buttonStyle}>
|
||||
View
|
||||
<IonIcon icon={arrowForwardOutline} />
|
||||
</IonButton>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
</IonCard>
|
||||
);
|
||||
})}
|
||||
</IonContent>
|
||||
|
||||
<IonFooter className="ion-padding-bottom">
|
||||
<IonRow className="ion-padding-start ion-padding-end ion-padding-bottom ion-padding-top">
|
||||
<IonCol size="12">
|
||||
<IonButton expand="block" routerLink="/demo-react-poll-app/page/add">
|
||||
Add new poll
|
||||
</IonButton>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
</IonFooter>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default View;
|
@@ -0,0 +1,28 @@
|
||||
.pollQuestion {
|
||||
|
||||
ion-card-title {
|
||||
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
ion-card-subtitle {
|
||||
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
ion-button {
|
||||
|
||||
margin-left: -1.5rem;
|
||||
|
||||
ion-icon {
|
||||
|
||||
font-size: 1rem;
|
||||
margin-left: 0.3rem;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
|
||||
|
||||
}
|
||||
}
|
@@ -0,0 +1,130 @@
|
||||
import { IonBackButton, IonButton, IonButtons, IonCard, IonCardHeader, IonCardSubtitle, IonCardTitle, IonCol, IonContent, IonFooter, IonHeader, IonIcon, IonPage, IonProgressBar, IonRow, IonTitle, IonToast, IonToolbar, useIonViewWillEnter, useIonPopover } from '@ionic/react';
|
||||
import { arrowRedoOutline, colorWandOutline } from 'ionicons/icons';
|
||||
import { useState } from 'react';
|
||||
import { useParams } from 'react-router';
|
||||
import { PollStore } from '../store';
|
||||
import { addVote } from '../store/PollStore';
|
||||
import { getPoll } from '../store/Selectors';
|
||||
import styles from "./ViewPoll.module.scss";
|
||||
import { getColors } from '../helpers/utils';
|
||||
import { SharePopover } from '../components/Share';
|
||||
import { useRef } from 'react';
|
||||
|
||||
const ViewPoll = () => {
|
||||
|
||||
const params = useParams();
|
||||
const poll = PollStore.useState(getPoll(params.id));
|
||||
|
||||
const answerRefs = useRef([]);
|
||||
const [ colors, setColors ] = useState({});
|
||||
const [ showToast, setShowToast ] = useState(false);
|
||||
const [ showVotes, setShowVotes ] = useState(false);
|
||||
|
||||
const [ present, dismiss ] = useIonPopover(SharePopover, { onHide: () => dismiss(), poll, setShowToast });
|
||||
|
||||
useIonViewWillEnter(() => {
|
||||
|
||||
const cardStyleColors = getColors(poll.color);
|
||||
setColors(cardStyleColors);
|
||||
});
|
||||
|
||||
const vote = answerId => {
|
||||
|
||||
if (!poll.voted) {
|
||||
|
||||
poll.answers.forEach((answer, index) => {
|
||||
|
||||
const pollAnswer = answerRefs.current[index];
|
||||
pollAnswer.classList.add("animate__fadeOut");
|
||||
});
|
||||
|
||||
setTimeout(() => addVote(params.id, answerId), 550);
|
||||
}
|
||||
}
|
||||
|
||||
const getAnswerPercentage = (totalVotes, answerVotes) => {
|
||||
|
||||
const percent = Math.round((answerVotes / totalVotes) * 100);
|
||||
return percent;
|
||||
}
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonButtons slot="start">
|
||||
<IonBackButton text="Ionic Polls" style={{ color: poll.color }} />
|
||||
</IonButtons>
|
||||
<IonTitle>Ionic Poll</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonContent fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large">Ionic Poll</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonCard className={ `${ styles.pollQuestion } animate__animated animate__fadeIn` }>
|
||||
<IonCardHeader style={{ backgroundColor: colors.backgroundColor }}>
|
||||
<IonCardTitle style={{ color: colors.textColor }}>{ poll.question }</IonCardTitle>
|
||||
<IonCardSubtitle className="ion-margin-bottom" style={{ color: colors.subTextColor }}>{ poll.timeLeft } left</IonCardSubtitle>
|
||||
<p style={{ color: colors.textColor, margin: "0" }}>{ poll.totalVotes } votes already</p>
|
||||
|
||||
</IonCardHeader>
|
||||
</IonCard>
|
||||
|
||||
<div className="animate__animated animate__fadeIn">
|
||||
{ poll.answers.map((answer, index) => {
|
||||
|
||||
const answerPercentage = getAnswerPercentage(poll.totalVotes, answer.votes);
|
||||
|
||||
return (
|
||||
<IonRow key={ `answer_${ answer.id }` } className={ `ion-align-items-center ion-justify-content-center ${ styles.pollAnswer }` }>
|
||||
<IonCol size="11">
|
||||
{ !poll.voted &&
|
||||
<IonButton className="animate__animated" ref={ ref => answerRefs.current[index] = ref } expand="block" fill={ answer.voted ? "solid" : "outline" } style={ answer.voted ? colors.votedButtonStyle : colors.notVotedbuttonStyle } onClick={ () => vote(answer.id) }>
|
||||
{ answer.answer }
|
||||
{ poll.voted && ` (${ answerPercentage }%)` }
|
||||
</IonButton>
|
||||
}
|
||||
|
||||
{ (poll.voted) &&
|
||||
<div id={ `answerVoted_${ answer.id }` }>
|
||||
<p style={{ color: poll.color }}>{ answer.answer } ({ answerPercentage })%</p>
|
||||
{ showVotes && <p style={{ color: poll.color }}>{ answer.votes } of { poll.totalVotes } total votes</p> }
|
||||
<IonProgressBar value={ answerPercentage / 100 } style={ colors.percentTrack } />
|
||||
</div>
|
||||
}
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<IonToast duration="2000" header="Poll" message="Copied to clipboard" onDidDismiss={ () => setShowToast(false) } isOpen={ showToast } color="dark" position="bottom" />
|
||||
</IonContent>
|
||||
|
||||
<IonFooter className="ion-padding-bottom">
|
||||
<IonRow className="ion-padding-start ion-padding-end ion-padding-bottom ion-padding-top">
|
||||
<IonCol size="6">
|
||||
<IonButton expand="block" style={ colors.votedButtonStyle } onClick={ (e) => present({ event: e.nativeEvent }) }>
|
||||
<IonIcon icon={ arrowRedoOutline } />
|
||||
Share
|
||||
</IonButton>
|
||||
</IonCol>
|
||||
|
||||
<IonCol size="6">
|
||||
<IonButton expand="block" fill="outline" style={ colors.notVotedbuttonStyle } onClick={ () => setShowVotes(!showVotes) } disabled={ !poll.voted }>
|
||||
<IonIcon icon={ colorWandOutline } />
|
||||
{ showVotes ? "Hide" : "Show" } votes
|
||||
</IonButton>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
</IonFooter>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default ViewPoll;
|
@@ -0,0 +1,40 @@
|
||||
.pollQuestion {
|
||||
|
||||
ion-card-title {
|
||||
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
ion-card-subtitle {
|
||||
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
ion-button {
|
||||
|
||||
margin-left: -1.5rem;
|
||||
|
||||
ion-icon {
|
||||
|
||||
font-size: 1rem;
|
||||
margin-left: 0.3rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pollAnswer {
|
||||
|
||||
p {
|
||||
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
|
||||
div {
|
||||
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
4
03_source/mobile/src/pages/DemoReactPollApp/TODO.md
Normal file
@@ -0,0 +1,4 @@
|
||||
# TODO
|
||||
|
||||
css temporary broken, ignored
|
||||
https://ionicreacthub.com/ionic-react-poll-app
|
@@ -0,0 +1,21 @@
|
||||
import { IonButton, IonCol, IonIcon, IonInput, IonItem, IonLabel, IonRow } from "@ionic/react";
|
||||
import { trashOutline } from "ionicons/icons";
|
||||
|
||||
export const PollAnswer = ({ index, value, change, remove}) => (
|
||||
|
||||
<IonRow className="ion-justify-content-center ion-align-items-center">
|
||||
<IonCol size="10">
|
||||
<IonItem lines="full">
|
||||
<IonLabel>
|
||||
<p>Option { index + 1 }</p>
|
||||
<IonInput type="text" inputmode="text" value={ value.answer } onIonChange={ e => change(e, index) } placeholder="Enter answer..." />
|
||||
</IonLabel>
|
||||
</IonItem>
|
||||
</IonCol>
|
||||
<IonCol size="2">
|
||||
<IonButton color="danger" fill="outline" onClick={ () => remove(value) }>
|
||||
<IonIcon icon={ trashOutline } />
|
||||
</IonButton>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
);
|
@@ -0,0 +1,13 @@
|
||||
import { IonCardSubtitle, IonCol, IonInput, IonItem } from "@ionic/react";
|
||||
|
||||
export const PollDuration = ({ label, value, setter }) => (
|
||||
|
||||
<IonCol size="4">
|
||||
<IonItem lines="full">
|
||||
<IonCol className="ion-text-center">
|
||||
<IonCardSubtitle>{ label }</IonCardSubtitle>
|
||||
<IonInput type="number" inputmode="numeric" value={ value } onIonChange={ e => setter(e.target.value) } />
|
||||
</IonCol>
|
||||
</IonItem>
|
||||
</IonCol>
|
||||
);
|
@@ -0,0 +1,43 @@
|
||||
import { IonItem, IonList, IonListHeader } from "@ionic/react";
|
||||
import { Share } from '@capacitor/share';
|
||||
import { Clipboard } from '@capacitor/clipboard';
|
||||
|
||||
export const SharePopover = ({ onHide, poll, setShowToast }) => {
|
||||
|
||||
const sharePoll = async () => {
|
||||
|
||||
const shareLink = `https://ionic-react-poll-app.netlify.app/page/view/${ poll.id }`;
|
||||
const title = `Check out this poll - ${ poll.question } | ${ poll.totalVotes } votes already`;
|
||||
|
||||
await Share.share({
|
||||
|
||||
title: title,
|
||||
text: title,
|
||||
url: shareLink,
|
||||
dialogTitle: 'Share this poll',
|
||||
});
|
||||
}
|
||||
|
||||
const copyLink = async () => {
|
||||
|
||||
await Clipboard.write({
|
||||
|
||||
string: `https://ionic-react-poll-app.netlify.app/page/view/${ poll.id }`
|
||||
});
|
||||
|
||||
onHide();
|
||||
|
||||
setShowToast(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<IonList>
|
||||
<IonListHeader>Share Poll</IonListHeader>
|
||||
<IonItem button onClick={ sharePoll }>Socials</IonItem>
|
||||
<IonItem button onClick={ copyLink }>Copy Link</IonItem>
|
||||
<IonItem lines="none" detail={false} button onClick={ onHide }>
|
||||
Close
|
||||
</IonItem>
|
||||
</IonList>
|
||||
);
|
||||
}
|
89
03_source/mobile/src/pages/DemoReactPollApp/helpers/utils.js
Normal file
@@ -0,0 +1,89 @@
|
||||
export const getColors = baseColor => {
|
||||
|
||||
const textColor = shadeColor(baseColor, 200);
|
||||
const subTextColor = shadeColor(baseColor, 120);
|
||||
const backgroundColor = baseColor;
|
||||
|
||||
return {
|
||||
|
||||
textColor,
|
||||
subTextColor,
|
||||
backgroundColor,
|
||||
notVotedbuttonStyle: {
|
||||
|
||||
"--color": baseColor,
|
||||
"--background-focused": baseColor,
|
||||
"--background-activated": baseColor,
|
||||
"--background": "white",
|
||||
"--border-color": baseColor
|
||||
},
|
||||
votedButtonStyle: {
|
||||
|
||||
"--color": shadeColor(baseColor, 200),
|
||||
"--background-focused": baseColor,
|
||||
"--background-activated": baseColor,
|
||||
"--background": baseColor,
|
||||
"--border-color": shadeColor(baseColor, 200)
|
||||
},
|
||||
percentTrack: {
|
||||
|
||||
"--background": shadeColor(baseColor, 70),
|
||||
"--progress-background": baseColor,
|
||||
height: "1rem"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const getCardStyle = baseColor => {
|
||||
|
||||
const textColor = shadeColor(baseColor, 200);
|
||||
const subTextColor = shadeColor(baseColor, 120);
|
||||
const backgroundColor = baseColor;
|
||||
const buttonColor = shadeColor(baseColor, 100);
|
||||
const buttonTextColor = shadeColor(baseColor, 0);
|
||||
|
||||
return {
|
||||
|
||||
textColor,
|
||||
subTextColor,
|
||||
backgroundColor,
|
||||
buttonStyle: {
|
||||
|
||||
"--color": buttonTextColor,
|
||||
"--background": buttonColor,
|
||||
"--background-focused": shadeColor(baseColor, 20),
|
||||
"--background-activated": shadeColor(baseColor, 20),
|
||||
},
|
||||
statusBadge: {
|
||||
|
||||
backgroundColor: "rgba(0, 0, 0, 0.1)",
|
||||
width: "fit-content",
|
||||
padding: "0.5rem",
|
||||
color: subTextColor,
|
||||
margin: 0,
|
||||
marginTop: "1rem",
|
||||
borderRadius: "5px"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const shadeColor = (color, percent) => {
|
||||
|
||||
var R = parseInt(color.substring(1,3),16);
|
||||
var G = parseInt(color.substring(3,5),16);
|
||||
var B = parseInt(color.substring(5,7),16);
|
||||
|
||||
R = parseInt(R * (100 + percent) / 100);
|
||||
G = parseInt(G * (100 + percent) / 100);
|
||||
B = parseInt(B * (100 + percent) / 100);
|
||||
|
||||
R = (R<255)?R:255;
|
||||
G = (G<255)?G:255;
|
||||
B = (B<255)?B:255;
|
||||
|
||||
var RR = ((R.toString(16).length==1)?"0"+R.toString(16):R.toString(16));
|
||||
var GG = ((G.toString(16).length==1)?"0"+G.toString(16):G.toString(16));
|
||||
var BB = ((B.toString(16).length==1)?"0"+B.toString(16):B.toString(16));
|
||||
|
||||
return "#"+RR+GG+BB;
|
||||
}
|
34
03_source/mobile/src/pages/DemoReactPollApp/index.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { IonIcon, IonLabel, IonRouterOutlet, IonTabBar, IonTabButton, IonTabs } from '@ionic/react';
|
||||
|
||||
import { cloudOutline, searchOutline } from 'ionicons/icons';
|
||||
import { Route, Redirect } from 'react-router';
|
||||
|
||||
import Add from './AppPages/Add';
|
||||
import View from './AppPages/View';
|
||||
import ViewPoll from './AppPages/ViewPoll';
|
||||
|
||||
import './style.scss';
|
||||
|
||||
function DemoReactPollApp() {
|
||||
return (
|
||||
<IonTabs>
|
||||
<IonRouterOutlet>
|
||||
<Route path="/demo-react-poll-app/page/view" exact={true}>
|
||||
<View />
|
||||
</Route>
|
||||
|
||||
<Route path="/demo-react-poll-app/page/view/:id" exact={true}>
|
||||
<ViewPoll />
|
||||
</Route>
|
||||
|
||||
<Route path="/demo-react-poll-app/page/add" exact={true}>
|
||||
<Add />
|
||||
</Route>
|
||||
|
||||
<Redirect exact path="/demo-react-poll-app" to="/demo-react-poll-app/page/view" />
|
||||
</IonRouterOutlet>
|
||||
</IonTabs>
|
||||
);
|
||||
}
|
||||
|
||||
export default DemoReactPollApp;
|
159
03_source/mobile/src/pages/DemoReactPollApp/store/PollStore.js
Normal file
@@ -0,0 +1,159 @@
|
||||
import { Store } from 'pullstate';
|
||||
|
||||
const PollStore = new Store({
|
||||
|
||||
polls: [
|
||||
{
|
||||
id: 1,
|
||||
question: "What is your favourite movie?",
|
||||
timeLeft: "2 hours, 17 mins",
|
||||
totalVotes: 137,
|
||||
color: "#759dc7",
|
||||
voted: false,
|
||||
answers: [
|
||||
|
||||
{
|
||||
id: 1,
|
||||
answer: "Avengers",
|
||||
votes: 76,
|
||||
voted: false,
|
||||
percent: 0
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
answer: "Taken 2",
|
||||
votes: 61,
|
||||
voted: false,
|
||||
percent: 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
question: "Do you prefer night or day?",
|
||||
timeLeft: "1 hours, 3 mins",
|
||||
totalVotes: 22,
|
||||
color: "#68bd8d",
|
||||
voted: false,
|
||||
answers: [
|
||||
|
||||
{
|
||||
id: 1,
|
||||
answer: "Night",
|
||||
votes: 11,
|
||||
voted: false,
|
||||
percent: 0
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
answer: "Day",
|
||||
votes: 8,
|
||||
voted: false,
|
||||
percent: 0
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
answer: "In the middle",
|
||||
votes: 3,
|
||||
voted: false,
|
||||
percent: 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
question: "Who is the better singer?",
|
||||
timeLeft: "1 day, 2 hours, 43 mins",
|
||||
totalVotes: 268,
|
||||
color: "#8d68bd",
|
||||
voted: false,
|
||||
answers: [
|
||||
|
||||
{
|
||||
id: 1,
|
||||
answer: "Abba",
|
||||
votes: 104,
|
||||
voted: false,
|
||||
percent: 0
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
answer: "Metallica",
|
||||
votes: 114,
|
||||
voted: false,
|
||||
percent: 0
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
answer: "Queen",
|
||||
votes: 50,
|
||||
voted: false,
|
||||
percent: 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
question: "Best type of food?",
|
||||
timeLeft: "4 days, 6 hours, 19 mins",
|
||||
totalVotes: 166,
|
||||
color: "#7c7c7c",
|
||||
voted: false,
|
||||
answers: [
|
||||
|
||||
{
|
||||
id: 1,
|
||||
answer: "Hamburger",
|
||||
votes: 76,
|
||||
voted: false,
|
||||
percent: 0
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
answer: "Hotdog",
|
||||
votes: 61,
|
||||
voted: false,
|
||||
percent: 0
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
answer: "Chips",
|
||||
votes: 10,
|
||||
voted: false,
|
||||
percent: 0
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
answer: "Steak",
|
||||
votes: 19,
|
||||
voted: false,
|
||||
percent: 0
|
||||
}
|
||||
]
|
||||
},
|
||||
]
|
||||
});
|
||||
|
||||
export default PollStore;
|
||||
|
||||
export const addVote = (pollId, answerId) => {
|
||||
|
||||
PollStore.update(state => {
|
||||
|
||||
const pollIndex = state.polls.findIndex(poll => poll.id === parseInt(pollId));
|
||||
const answerIndex = state.polls[pollIndex].answers.findIndex(answer => answer.id === parseInt(answerId));
|
||||
|
||||
state.polls[pollIndex].voted = true;
|
||||
state.polls[pollIndex].totalVotes = state.polls[pollIndex].totalVotes + 1;
|
||||
state.polls[pollIndex].answers[answerIndex].votes = state.polls[pollIndex].answers[answerIndex].votes + 1;
|
||||
state.polls[pollIndex].answers[answerIndex].voted = true;
|
||||
});
|
||||
}
|
||||
|
||||
export const addPoll = (poll) => {
|
||||
|
||||
PollStore.update(state => {
|
||||
|
||||
state.polls.unshift(poll);
|
||||
});
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
const getState = state => state;
|
||||
|
||||
// General getters
|
||||
export const getPolls = createSelector(getState, state => state.polls);
|
||||
|
||||
// More specific getters
|
||||
export const getPoll = pollId => createSelector(getState, state => state.polls.filter(poll => poll.id === parseInt(pollId))[0]);
|
||||
// export const getChat = contactId => createSelector(getState, state => state.chats.filter(c => parseInt(c.contact_id) === parseInt(contactId))[0].chats);
|
||||
// export const getContact = contactId => createSelector(getState, state => state.contacts.filter(c => parseInt(c.id) === parseInt(contactId))[0]);
|
||||
|
||||
// export const getChatNotificationCount = contactId => createSelector(getState, state => (state.chats.filter(c => parseInt(c.contact_id) === parseInt(contactId))[0].chats).filter(chat => chat.read === false));
|
@@ -0,0 +1,2 @@
|
||||
|
||||
export { default as PollStore } from "./PollStore";
|
84
03_source/mobile/src/pages/DemoReactPollApp/style.scss
Normal file
@@ -0,0 +1,84 @@
|
||||
/* Ionic Variables and Theming. For more info, please see:
|
||||
http://ionicframework.com/docs/theming/ */
|
||||
|
||||
/** Ionic CSS Variables **/
|
||||
.demo-react-poll-app {
|
||||
* {
|
||||
/** primary **/
|
||||
--ion-color-primary: #3880ff;
|
||||
--ion-color-primary-rgb: 56, 128, 255;
|
||||
--ion-color-primary-contrast: #ffffff;
|
||||
--ion-color-primary-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-primary-shade: #3171e0;
|
||||
--ion-color-primary-tint: #4c8dff;
|
||||
|
||||
/** secondary **/
|
||||
--ion-color-secondary: #3dc2ff;
|
||||
--ion-color-secondary-rgb: 61, 194, 255;
|
||||
--ion-color-secondary-contrast: #ffffff;
|
||||
--ion-color-secondary-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-secondary-shade: #36abe0;
|
||||
--ion-color-secondary-tint: #50c8ff;
|
||||
|
||||
/** tertiary **/
|
||||
--ion-color-tertiary: #5260ff;
|
||||
--ion-color-tertiary-rgb: 82, 96, 255;
|
||||
--ion-color-tertiary-contrast: #ffffff;
|
||||
--ion-color-tertiary-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-tertiary-shade: #4854e0;
|
||||
--ion-color-tertiary-tint: #6370ff;
|
||||
|
||||
/** success **/
|
||||
--ion-color-success: #2dd36f;
|
||||
--ion-color-success-rgb: 45, 211, 111;
|
||||
--ion-color-success-contrast: #ffffff;
|
||||
--ion-color-success-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-success-shade: #28ba62;
|
||||
--ion-color-success-tint: #42d77d;
|
||||
|
||||
/** warning **/
|
||||
--ion-color-warning: #ffc409;
|
||||
--ion-color-warning-rgb: 255, 196, 9;
|
||||
--ion-color-warning-contrast: #000000;
|
||||
--ion-color-warning-contrast-rgb: 0, 0, 0;
|
||||
--ion-color-warning-shade: #e0ac08;
|
||||
--ion-color-warning-tint: #ffca22;
|
||||
|
||||
/** danger **/
|
||||
--ion-color-danger: #eb445a;
|
||||
--ion-color-danger-rgb: 235, 68, 90;
|
||||
--ion-color-danger-contrast: #ffffff;
|
||||
--ion-color-danger-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-danger-shade: #cf3c4f;
|
||||
--ion-color-danger-tint: #ed576b;
|
||||
|
||||
/** dark **/
|
||||
--ion-color-dark: #222428;
|
||||
--ion-color-dark-rgb: 34, 36, 40;
|
||||
--ion-color-dark-contrast: #ffffff;
|
||||
--ion-color-dark-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-dark-shade: #1e2023;
|
||||
--ion-color-dark-tint: #383a3e;
|
||||
|
||||
/** medium **/
|
||||
--ion-color-medium: #92949c;
|
||||
--ion-color-medium-rgb: 146, 148, 156;
|
||||
--ion-color-medium-contrast: #ffffff;
|
||||
--ion-color-medium-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-medium-shade: #808289;
|
||||
--ion-color-medium-tint: #9d9fa6;
|
||||
|
||||
/** light **/
|
||||
--ion-color-light: #f4f5f8;
|
||||
--ion-color-light-rgb: 244, 245, 248;
|
||||
--ion-color-light-contrast: #000000;
|
||||
--ion-color-light-contrast-rgb: 0, 0, 0;
|
||||
--ion-color-light-shade: #d7d8da;
|
||||
--ion-color-light-tint: #f5f6f9;
|
||||
}
|
||||
|
||||
ion-footer {
|
||||
border-top: 4px solid rgb(243, 243, 243);
|
||||
background-color: white;
|
||||
}
|
||||
}
|