Compare commits

...

5 Commits

261 changed files with 98195 additions and 487 deletions

View File

@@ -1,6 +1,15 @@
{
"tabWidth": 2,
"semi": true,
"singleQuote": true,
"trailingComma": "es5",
"printWidth": 100
"printWidth": 160,
"overrides": [
{
"files": "src/App.tsx",
"options": {
"printWidth": 240
}
}
]
}

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",
@@ -24,8 +25,10 @@
"date-fns": "^2.25.0",
"ionicons": "^7.1.2",
"leaflet": "^1.9.4",
"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",
@@ -40,7 +43,7 @@
"react-use": "^17.6.0",
"reselect": "^4.0.0",
"sass": "^1.85.1",
"swiper": "^9.1.1",
"swiper": "^11.2.8",
"use-sound": "^5.0.0",
"zod": "^3.24.2"
},
@@ -73,7 +76,7 @@
"@types/react-router-dom": "^5.3.3",
"@vitejs/plugin-react": "^4.3.4",
"lint-staged": "^13.2.0",
"prettier": "^2.8.6",
"prettier": "^3.5.3",
"typescript": "^5.8.2",
"vite": "^6.2.0"
},

Binary file not shown.

After

Width:  |  Height:  |  Size: 930 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 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: 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: 23 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: 42 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

@@ -66,7 +66,7 @@ import PrivacyAgreement from './pages/PrivacyAgreement';
import AppRoute from './AppRoute';
//
import DemoReactShop from './pages/DemoReactShop';
import DemoWeatherApp from './pages/WeatherDemo';
import DemoWeatherApp from './pages/DemoWeatherApp';
import DemoClubHouse from './pages/DemoClubHouse';
import DemoScoreBoard from './pages/DemoScoreBoard';
import DemoQuoteApp from './pages/DemoQuoteApp';
@@ -75,6 +75,22 @@ import DemoQrScanner from './pages/DemoQrScanner';
import DemoShopAppUi from './pages/DemoShopAppUi';
// DemoDictionaryApp
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();
@@ -100,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();
@@ -130,18 +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_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} />
@@ -150,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

@@ -1,8 +1,13 @@
import {
IonButton,
IonButtons,
IonCard,
IonCardContent,
IonCardSubtitle,
IonCardTitle,
IonCol,
IonContent,
IonGrid,
IonHeader,
IonIcon,
IonPage,
@@ -11,54 +16,27 @@ import {
IonToolbar,
useIonRouter,
} from '@ionic/react';
import { bookOutline, chevronBackOutline, heart, search } from 'ionicons/icons';
import { useStoreState } from 'pullstate';
import { useRef } from 'react';
import { WordStore } from '../store';
import { getFavourites, getSearchCount } from '../store/Selectors';
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 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 pageRef = useRef();
const favourites = useStoreState(WordStore, getFavourites);
const searchCount = useStoreState(WordStore, getSearchCount);
function handleBackClick() {
router.goBack();
}
return (
<IonPage>
<IonPage ref={pageRef}>
<IonHeader>
<IonToolbar>
<IonTitle>My Weather</IonTitle>
<IonButtons slot="end">
<IonButton onClick={() => getCurrentPosition()}>
<IonIcon icon={refreshOutline} color="primary" />
</IonButton>
</IonButtons>
<IonTitle>Dashboard</IonTitle>
<IonButtons slot="start">
<IonButton onClick={() => handleBackClick()}>
@@ -71,25 +49,66 @@ function Tab1() {
<IonHeader collapse="condense">
<IonToolbar>
<IonTitle size="large">Dashboard</IonTitle>
<IonButtons slot="start">
<IonButton onClick={() => handleBackClick()}>
<IonIcon icon={chevronBackOutline} color="primary" />
</IonButton>
</IonButtons>
</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>
<IonGrid>
<IonRow className={`animate__animated animate__faster animate__fadeIn`}>
<IonCol size="12">
<IonCard>
<IonCardContent>
<IonIcon icon={bookOutline} color="primary" style={{ fontSize: '2rem' }} />
<IonCardTitle>Ionic Dictionary App</IonCardTitle>
<p>Based on the English language</p>
</IonCardContent>
</IonCard>
</IonCol>
</IonRow>
<div style={{ marginTop: '-1.5rem' }}>
{currentWeather ? (
<CurrentWeather currentWeather={currentWeather} />
) : (
<SkeletonDashboard />
)}
</div>
<IonRow className={`animate__animated animate__faster animate__fadeIn`}>
<IonCol size="12">
<IonCard>
<IonCardContent>
<IonCardTitle>Did you know?</IonCardTitle>
<p>There are 171, 146 words in the English language!</p>
<IonButton expand="block" className="ion-margin-top" routerLink="/search">
Search now &rarr;
</IonButton>
</IonCardContent>
</IonCard>
</IonCol>
</IonRow>
<IonRow className={`animate__animated animate__faster animate__fadeIn`}>
<IonCol size="6">
<IonCard routerLink="/favourites">
<IonCardContent className="ion-text-center">
<IonIcon icon={heart} color="primary" />
<IonCardTitle>{favourites.length}</IonCardTitle>
<IonCardSubtitle>Favourites</IonCardSubtitle>
</IonCardContent>
</IonCard>
</IonCol>
<IonCol size="6">
<IonCard routerLink="/favourites">
<IonCardContent className="ion-text-center">
<IonIcon icon={search} color="primary" />
<IonCardTitle>{searchCount}</IonCardTitle>
<IonCardSubtitle>Searches</IonCardSubtitle>
</IonCardContent>
</IonCard>
</IonCol>
</IonRow>
</IonGrid>
</IonContent>
</IonPage>
);
}
};
export default Tab1;

View File

@@ -2,6 +2,7 @@ import {
IonButton,
IonCol,
IonContent,
IonGrid,
IonHeader,
IonPage,
IonRow,
@@ -9,30 +10,32 @@ import {
IonTitle,
IonToolbar,
} from '@ionic/react';
import { useState } from 'react';
import { CurrentWeather } from '../components/CurrentWeather';
import { useState, useRef } from 'react';
import { NoSearch } from '../components/NoSearch';
import { NoResultsWordCard, WordCard } from '../components/WordCard';
import { WordStore } from '../store';
import { searchWord } from '../utils';
function Tab2() {
const [search, setSearch] = useState('');
const [currentWeather, setCurrentWeather] = useState(false);
const Tab2 = () => {
const pageRef = useRef();
const [searchTerm, setSearchTerm] = useState('');
const [searchResult, setSearchResult] = useState(false);
const [animatedClass, setAnimatedClass] = useState('');
const performSearch = async () => {
getAddress(search);
};
setAnimatedClass('animate__slideOutRight');
const result = searchTerm !== '' ? await searchWord(searchTerm) : undefined;
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();
setTimeout(() => setSearchResult(result === undefined ? 'none' : result), 250);
setTimeout(() => setAnimatedClass('animate__slideInLeft'), 250);
if (data && data.current && data.location) {
setCurrentWeather(data);
}
WordStore.update((s) => {
s.searchCount++;
});
};
return (
<IonPage>
<IonPage ref={pageRef}>
<IonHeader>
<IonToolbar>
<IonTitle>Search</IonTitle>
@@ -45,37 +48,36 @@ function Tab2() {
</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>
<IonGrid>
<IonRow className="ion-align-items-center">
<IonCol size="9">
<IonSearchbar
animated
value={searchTerm}
onIonChange={(e) => setSearchTerm(e.target.value)}
/>
</IonCol>
<IonCol size="5">
<IonButton
expand="block"
className="ion-margin-start ion-margin-end"
onClick={performSearch}
>
Search
</IonButton>
</IonCol>
</IonRow>
<IonCol size="3">
<IonButton color="primary" 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>
{searchResult && searchResult !== 'none' && (
<WordCard word={searchResult} animatedClass={animatedClass} pageRef={pageRef} />
)}
</div>
{searchResult && searchResult === 'none' && (
<NoResultsWordCard word={searchResult} animatedClass={animatedClass} />
)}
{!searchResult && <NoSearch />}
</IonGrid>
</IonContent>
</IonPage>
);
}
};
export default Tab2;

View File

@@ -0,0 +1,45 @@
import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar } from '@ionic/react';
import { useStoreState } from 'pullstate';
import { useRef, useState } from 'react';
import { NoFavourites } from '../components/NoFavourites';
import { WordCard } from '../components/WordCard';
import { WordStore } from '../store';
import { getFavourites } from '../store/Selectors';
const Tab3 = () => {
const pageRef = useRef();
const favourites = useStoreState(WordStore, getFavourites);
const [animatedClass, setAnimatedClass] = useState('animate__slideInLeft');
return (
<IonPage ref={pageRef}>
<IonHeader>
<IonToolbar>
<IonTitle>Favourites</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent fullscreen>
<IonHeader collapse="condense">
<IonToolbar>
<IonTitle size="large">Favourites</IonTitle>
</IonToolbar>
</IonHeader>
{favourites.map((favourite, index) => {
return (
<WordCard
key={index}
word={favourite}
animatedClass={animatedClass}
pageRef={pageRef}
/>
);
})}
{favourites.length < 1 && <NoFavourites />}
</IonContent>
</IonPage>
);
};
export default Tab3;

View File

@@ -0,0 +1,22 @@
import { IonCol, IonLabel, IonRow } from '@ionic/react';
export const NoFavourites = () => (
<IonRow className="ion-text-center ion-justify-content-center ion-margin-top ion-padding-top">
<IonCol size="10" className="ion-padding-top ion-margin-top">
<IonLabel className="ion-padding-top ion-margin-top">
<h2>You don't have any favourites yet!</h2>
<p>
Any time you see the heart icon, press it to add the related word to your favourites and
quickly access it from here.
</p>
<lottie-player
src="https://assets9.lottiefiles.com/packages/lf20_LK2KVy.json"
background="transparent"
speed="1"
loop
autoplay
></lottie-player>
</IonLabel>
</IonCol>
</IonRow>
);

View File

@@ -0,0 +1,23 @@
import { IonCol, IonLabel, IonRow } from '@ionic/react';
export const NoSearch = () => (
<IonRow className="ion-text-center ion-justify-content-center ion-margin-top">
<IonCol size="10">
<IonLabel>
<h2>Search for a word in the English language</h2>
<p>
This app will give you word meaninigs, phonetics, origin and also an audio clip so you can
hear what it sounds like.
</p>
<lottie-player
src="https://assets7.lottiefiles.com/packages/lf20_n2m0isqh.json"
mode="bounce"
background="transparent"
speed="0.8"
loop
autoplay
></lottie-player>
</IonLabel>
</IonCol>
</IonRow>
);

View File

@@ -0,0 +1,129 @@
import {
IonBadge,
IonButton,
IonCard,
IonCardContent,
IonCardSubtitle,
IonCardTitle,
IonCol,
IonIcon,
IonNote,
IonRow,
useIonModal,
} from '@ionic/react';
import { checkmarkCircleOutline, chevronForward, closeCircleOutline } from 'ionicons/icons';
import WordModal from './WordModal';
export const WordCard = ({ word, animatedClass, pageRef }) => {
const closeModal = () => {
hideModal();
};
const openModal = () => {
showModal({
presentingElement: pageRef.current,
onDidDismiss: hideModal,
});
};
const [showModal, hideModal] = useIonModal(WordModal, {
dismiss: closeModal,
word,
});
return (
<IonRow className={`animate__animated animate__faster ${animatedClass}`}>
<IonCol size="12">
<IonCard>
<IonCardContent>
<IonCardTitle>{word.word}</IonCardTitle>
<div className="ion-padding-bottom ion-padding-top">
{word.meanings &&
word.meanings.map((meaning, index) => {
return (
<span key={index}>
<IonBadge color="primary">{meaning.partOfSpeech}</IonBadge>
&nbsp;
</span>
);
})}
</div>
<IonNote color="white">{word.origin}</IonNote>
<IonRow className="ion-padding-top ion-align-items-center ion-justify-content-center ion-text-center">
<IonCol size="4">
<IonCardTitle>{word.meanings.length}</IonCardTitle>
<IonCardSubtitle>meanings</IonCardSubtitle>
</IonCol>
<IonCol size="4">
<IonCardTitle>{word.phonetics.length}</IonCardTitle>
<IonCardSubtitle>phonetics</IonCardSubtitle>
</IonCol>
<IonCol size="4">
<IonCardTitle>
<IonIcon
icon={
word.phonetics[0] && word.phonetics[0].audio
? checkmarkCircleOutline
: closeCircleOutline
}
/>
</IonCardTitle>
<IonCardSubtitle>audio</IonCardSubtitle>
</IonCol>
</IonRow>
<IonRow>
<IonCol size="12">
<IonButton color="primary" expand="block" onClick={openModal}>
View&nbsp;
<IonIcon icon={chevronForward} />
</IonButton>
</IonCol>
</IonRow>
</IonCardContent>
</IonCard>
</IonCol>
</IonRow>
);
};
export const NoResultsWordCard = ({ word, animatedClass }) => {
return (
<IonRow className={`animate__animated animate__faster ${animatedClass}`}>
<IonCol size="12">
<IonCard>
<IonCardContent>
<IonCardTitle>Whoops...</IonCardTitle>
<div className="ion-padding-bottom ion-padding-top">
<IonBadge color="primary">no results</IonBadge>&nbsp;
<IonBadge color="primary">found</IonBadge>
</div>
<IonNote color="white">
No results have been found for your search criteria! Please try another word.
</IonNote>
<IonRow className="ion-padding-top ion-align-items-center ion-justify-content-center ion-text-center">
<IonCol size="4">
<IonCardTitle>0</IonCardTitle>
<IonCardSubtitle>meanings</IonCardSubtitle>
</IonCol>
<IonCol size="4">
<IonCardTitle>0</IonCardTitle>
<IonCardSubtitle>phonetics</IonCardSubtitle>
</IonCol>
<IonCol size="4">
<IonCardTitle>
<IonIcon icon={closeCircleOutline} />
</IonCardTitle>
<IonCardSubtitle>audio</IonCardSubtitle>
</IonCol>
</IonRow>
</IonCardContent>
</IonCard>
</IonCol>
</IonRow>
);
};

View File

@@ -0,0 +1,10 @@
import { IonText } from "@ionic/react";
export const WordCardHeading = ({ text }) => (
<div style={{ marginTop: "-1.5rem" }}>
<IonText color="light">
<h2 className="ion-padding-start">{ text }</h2>
</IonText>
</div>
);

View File

@@ -0,0 +1,18 @@
import { IonBadge } from '@ionic/react';
export const WordMeaning = ({ meaning, index }) => (
<div className={index > 0 ? 'ion-padding-top' : ''}>
<IonBadge key={index} color="primary">
{meaning.partOfSpeech}
</IonBadge>
<br />
{meaning.definitions.map((definition, index2) => {
return (
<p key={`definition_${index2}`} className={index2 > 0 ? 'ion-padding-top' : ''}>
{index2 + 1}.&nbsp;
{definition.definition}
</p>
);
})}
</div>
);

View File

@@ -0,0 +1,122 @@
import {
IonBadge,
IonButton,
IonButtons,
IonCard,
IonCardContent,
IonCardHeader,
IonCardSubtitle,
IonCardTitle,
IonCol,
IonContent,
IonGrid,
IonHeader,
IonIcon,
IonNote,
IonPage,
IonRow,
IonText,
IonTitle,
IonToolbar,
} from '@ionic/react';
import { heart, heartOutline, play } from 'ionicons/icons';
import { useStoreState } from 'pullstate';
import { WordStore } from '../store';
import { getFavourites } from '../store/Selectors';
import { addToFavourites } from '../store/WordStore';
import { WordCardHeading } from './WordCardHeading';
import { WordMeaning } from './WordMeaning';
const WordModal = ({ dismiss, word }) => {
const favourites = useStoreState(WordStore, getFavourites);
const isFavourite = favourites.includes(word);
const audio = word.phonetics[0] ? word.phonetics[0].audio : false;
const playAudio = () => {
const audioElement = new Audio(`https:${audio}`);
audioElement.play();
};
return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<IonButton onClick={() => addToFavourites(word)}>
<IonIcon icon={isFavourite ? heart : heartOutline} />
</IonButton>
</IonButtons>
<IonTitle>View Word</IonTitle>
<IonButtons slot="end">
<IonButton onClick={dismiss}>Close</IonButton>
</IonButtons>
</IonToolbar>
</IonHeader>
<IonContent fullscreen>
<IonGrid>
<IonRow className="animate__animated animate__faster animate__slideInUp">
<IonCol size="12">
<IonCard>
<IonCardContent>
<IonCardTitle>{word.word}</IonCardTitle>
<div className="ion-padding-bottom ion-padding-top">
{word.meanings &&
word.meanings.map((meaning, index) => {
return (
<span key={`meaning_${index}`}>
<IonBadge key={index} color="primary">
{meaning.partOfSpeech}
</IonBadge>
&nbsp;
</span>
);
})}
</div>
<IonNote color="white">{word.origin}</IonNote>
</IonCardContent>
</IonCard>
</IonCol>
</IonRow>
{audio && (
<IonRow className="animate__animated animate__faster animate__slideInUp">
<IonCol size="12">
<WordCardHeading text="Audio Clip" />
<IonCard>
<IonCardContent>
<IonRow>
<IonCol size="12">
<IonButton color="primary" expand="block" onClick={playAudio}>
<IonIcon icon={play} />
</IonButton>
</IonCol>
</IonRow>
</IonCardContent>
</IonCard>
</IonCol>
</IonRow>
)}
<IonRow className="animate__animated animate__faster animate__slideInUp">
<IonCol size="12">
<WordCardHeading text="Meanings" />
<IonCard>
<IonCardContent>
{word.meanings &&
word.meanings.map((meaning, index) => {
return <WordMeaning key={index} index={index} meaning={meaning} />;
})}
</IonCardContent>
</IonCard>
</IonCol>
</IonRow>
</IonGrid>
</IonContent>
</IonPage>
);
};
export default WordModal;

View File

@@ -1,38 +1,44 @@
import { IonIcon, IonLabel, IonRouterOutlet, IonTabBar, IonTabButton, IonTabs } from '@ionic/react';
import { cloudOutline, searchOutline } from 'ionicons/icons';
import { cloudOutline, heart, search, searchOutline, statsChart } from 'ionicons/icons';
import { Route, Redirect } from 'react-router';
import Tab1 from './AppPages/Tab1';
import Tab2 from './AppPages/Tab2';
import Tab3 from './AppPages/Tab3';
function DemoWeatherApp() {
import './style.scss';
function DemoDictionaryApp() {
return (
<IonTabs>
<IonTabs className="demo-dictionary-app">
<IonRouterOutlet>
<Route exact path="/demo-weather-app/tab1">
<Route exact path="/demo-dictionary-app/dashboard">
<Tab1 />
</Route>
<Route exact path="/demo-weather-app/tab2">
<Route exact path="/demo-dictionary-app/search">
<Tab2 />
</Route>
<Route exact path="/demo-weather-app">
<Redirect to="/demo-weather-app/tab1" />
<Route path="/demo-dictionary-app/favourites">
<Tab3 />
</Route>
<Redirect exact path="/demo-dictionary-app" to="/demo-dictionary-app/dashboard" />
</IonRouterOutlet>
{/* */}
<IonTabBar slot="bottom">
<IonTabButton tab="tab1" href="/demo-weather-app/tab1">
<IonIcon icon={cloudOutline} />
<IonLabel>Dashboard</IonLabel>
<IonTabButton tab="dashboard" href="/demo-dictionary-app/dashboard">
<IonIcon icon={statsChart} />
</IonTabButton>
<IonTabButton tab="tab2" href="/demo-weather-app/tab2">
<IonIcon icon={searchOutline} />
<IonLabel>Search</IonLabel>
<IonTabButton tab="search" href="/demo-dictionary-app/search">
<IonIcon icon={search} />
</IonTabButton>
<IonTabButton tab="favourites" href="/demo-dictionary-app/favourites">
<IonIcon icon={heart} />
</IonTabButton>
</IonTabBar>
</IonTabs>
);
}
export default DemoWeatherApp;
export default DemoDictionaryApp;

View File

@@ -0,0 +1,8 @@
import { createSelector } from 'reselect';
const getState = state => state;
// General getters
export const getFavourites = createSelector(getState, state => state.favourites);
export const getPopularWords = createSelector(getState, state => state.popularWords);
export const getSearchCount = createSelector(getState, state => state.searchCount);

View File

@@ -0,0 +1,29 @@
import { Store } from "pullstate";
const WordStore = new Store({
favourites: [],
popularWords: [],
searchCount: 0
});
export default WordStore;
export const addToFavourites = (passedWord) => {
const currentFavourites = WordStore.getRawState().favourites;
const added = !currentFavourites.includes(passedWord);
WordStore.update(s => {
if (currentFavourites.includes(passedWord)) {
s.favourites = currentFavourites.filter(word => word !== passedWord);
} else {
s.favourites = [ ...s.favourites, passedWord ];
}
});
return added;
}

View File

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

View File

@@ -1,103 +1,240 @@
#about-page {
ion-toolbar {
position: absolute;
/* Ionic Variables and Theming. For more info, please see:
http://ionicframework.com/docs/theming/ */
top: 0;
left: 0;
right: 0;
/** Ionic CSS Variables **/
.demo-dictionary-app {
* {
/** primary **/
--ion-color-primary: #953cd0;
--ion-color-primary-rgb: 149, 60, 208;
--ion-color-primary-contrast: #ffffff;
--ion-color-primary-contrast-rgb: 255, 255, 255;
--ion-color-primary-shade: #8335b7;
--ion-color-primary-tint: #a050d5;
--background: transparent;
--color: white;
/** 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: #ffffff;
--ion-color-light-rgb: 255, 255, 255;
--ion-color-light-contrast: #000000;
--ion-color-light-contrast-rgb: 0, 0, 0;
--ion-color-light-shade: #e0e0e0;
--ion-color-light-tint: #ffffff;
--ion-background-color: #1e1b27 !important;
--ion-tab-bar-color-selected: #953cd0;
--ion-tab-bar-color: #412f6e;
--ion-text-color: white;
--ion-tab-bar-background: #191620;
--ion-toolbar-background: #191620 !important;
--ion-item-background: #000000 !important;
--ion-card-background: #272333 !important;
--ion-modal-background: #272333 !important;
}
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;
ion-tab-bar {
--border-style: none;
border: none;
}
/*
* iOS Only
* Dark Colors
* -------------------------------------------
*/
.ios .about-info {
--ion-padding: 19px;
ion-modal {
-ms-overflow-style: none; /* for Internet Explorer, Edge */
scrollbar-width: none; /* for Firefox */
overflow-y: scroll;
}
.ios .about-info h3 {
font-weight: 700;
body {
overflow: hidden !important;
--ion-color-primary: #953cd0;
--ion-color-primary-rgb: 149, 60, 208;
--ion-color-primary-contrast: #ffffff;
--ion-color-primary-contrast-rgb: 255, 255, 255;
--ion-color-primary-shade: #8335b7;
--ion-color-primary-tint: #a050d5;
--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: #ffffff;
--ion-color-light-rgb: 255, 255, 255;
--ion-color-light-contrast: #000000;
--ion-color-light-contrast-rgb: 0, 0, 0;
--ion-color-light-shade: #e0e0e0;
--ion-color-light-tint: #ffffff;
}
/*
* 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;
}
/*
* 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;
}
}
#date-input-popover {
--offset-y: -var(--ion-safe-area-bottom);
--max-width: 90%;
--width: 336px;
}

View File

@@ -0,0 +1,21 @@
import { WordStore } from './store';
const API_URL = 'https://api.dictionaryapi.dev/api/v2/entries/en/';
export const searchWord = async (word, returnOne = true) => {
const response = await fetch(`${API_URL}${word.toLowerCase()}`);
const data = await response.json();
return returnOne ? data[0] : data;
};
export const fetchPopularWords = async () => {
const words = ['mobile', 'applications', 'ionic', 'framework'];
words.forEach(async (word) => {
const wordData = await searchWord(word, false);
WordStore.update((s) => {
s.popularWords = [...s.popularWords, wordData[0]];
});
});
};

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>
@@ -252,14 +320,60 @@ const SettingsPage: React.FC<SettingsProps> = ({
<IonIcon icon={chevronForwardOutline}></IonIcon>
</IonItem>
</IonList>
<IonList inset={false}>
<IonItem button={true} onClick={() => router.push(paths.DEMO_DICTIONARY_APP, 'forward')}>
<IonIcon slot="start" icon={cart} size="large"></IonIcon>
<IonLabel>Demo Dictionary App</IonLabel>
<IonIcon icon={chevronForwardOutline}></IonIcon>
</IonItem>
</IonList>
<IonList inset={false}>
<IonItem button={true} onClick={() => router.push(paths.DEMO_RECIPE_APP, 'forward')}>
<IonIcon slot="start" icon={cart} size="large"></IonIcon>
<IonLabel>Demo Recipe App</IonLabel>
<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;

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