This commit is contained in:
louiscklaw
2025-06-05 11:29:42 +08:00
parent 8c46a93e61
commit d909805283
207 changed files with 10412 additions and 46 deletions

View File

@@ -13,6 +13,7 @@ dependencies {
implementation project(':capacitor-clipboard')
implementation project(':capacitor-geolocation')
implementation project(':capacitor-preferences')
implementation project(':capacitor-share')
}

View File

@@ -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')

View File

@@ -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

View File

@@ -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",

Binary file not shown.

After

Width:  |  Height:  |  Size: 930 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 358 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 424 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 930 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

View File

@@ -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={() => {

View File

@@ -0,0 +1,7 @@
.view-post-footer {
background-color: white;
padding-left: 1rem;
padding-right: 1rem;
padding-bottom: 1rem;
}

View File

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

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

View File

@@ -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%;
}

View File

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

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

View 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! Youre 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. Were thrilled by the communitys 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 Im excited to announce Identity Vault 5.0, the newest version of Ionics 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 doesnt mean were 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? Its 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 Im excited to share some news about Stencil, Ionics open source toolchain that generates small, fast, and 100% standards-based Web Components that run in every browser. As you might have noticed, weve 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, lets talk about some of the improvements coming in Framework v6 and how you can get access to these improvements today."
}
];

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

View File

@@ -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={{

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

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

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

View File

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

View File

@@ -0,0 +1 @@
export { default as SettingsStore } from "./SettingsStore";

File diff suppressed because one or more lines are too long

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

View File

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

View File

@@ -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? Its 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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

View File

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

View File

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

View File

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

View File

@@ -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 {
}
}

View File

@@ -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 } />
&nbsp; Share
</IonButton>
</IonCol>
<IonCol size="6">
<IonButton expand="block" fill="outline" style={ colors.notVotedbuttonStyle } onClick={ () => setShowVotes(!showVotes) } disabled={ !poll.voted }>
<IonIcon icon={ colorWandOutline } />
&nbsp; { showVotes ? "Hide" : "Show" } votes
</IonButton>
</IonCol>
</IonRow>
</IonFooter>
</IonPage>
);
};
export default ViewPoll;

View File

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

View File

@@ -0,0 +1,4 @@
# TODO
css temporary broken, ignored
https://ionicreacthub.com/ionic-react-poll-app

View File

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

View File

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

View File

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

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

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

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

View File

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

View File

@@ -0,0 +1,2 @@
export { default as PollStore } from "./PollStore";

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

Some files were not shown because too many files have changed in this diff Show More