update demo-react-notes,

This commit is contained in:
louiscklaw
2025-06-08 19:07:48 +08:00
parent c11dba6297
commit bc731ea2b8
13 changed files with 606 additions and 126 deletions

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,21 @@
import './ExploreContainer.css';
const ExploreContainer = (): React.JSX.Element => {
return (
<div className="container">
<strong>Ready to create an app?</strong>
<p>
Start with Ionic{' '}
<a
target="_blank"
rel="noopener noreferrer"
href="https://ionicframework.com/docs/components"
>
UI Components
</a>
</p>
</div>
);
};
export default ExploreContainer;

View File

@@ -1,38 +1,27 @@
import { IonIcon, IonLabel, IonRouterOutlet, IonTabBar, IonTabButton, IonTabs } from '@ionic/react';
import { IonRouterOutlet, IonTabs } from '@ionic/react';
import { cloudOutline, searchOutline } from 'ionicons/icons';
import { Route, Redirect } from 'react-router';
import Tab1 from './AppPages/Tab1';
import Tab2 from './AppPages/Tab2';
// import Tab1 from './AppPages/Tab1';
// import Tab2 from './AppPages/Tab2';
import './style.scss';
import './theme/variables.scss';
import Home from './pages/Home';
import Add from './pages/Add';
function DemoReactNotes() {
return (
<IonTabs>
<IonTabs className="demo-react-notes">
<IonRouterOutlet>
<Route exact path="/demo-react-notes/tab1">
<Tab1 />
<Route exact path="/demo-react-notes/home">
<Home />
</Route>
<Route exact path="/demo-react-notes/tab2">
<Tab2 />
<Route exact path="/demo-react-notes/add">
<Add />
</Route>
<Redirect exact path="/demo-react-notes" to="/demo-react-notes/tab1" />
<Redirect exact path="/demo-react-notes" to="/demo-react-notes/home" />
</IonRouterOutlet>
{/* */}
<IonTabBar slot="bottom">
<IonTabButton tab="tab1" href="/demo-react-notes/tab1">
<IonIcon icon={cloudOutline} />
<IonLabel>Dashboard</IonLabel>
</IonTabButton>
<IonTabButton tab="tab2" href="/demo-react-notes/tab2">
<IonIcon icon={searchOutline} />
<IonLabel>Search</IonLabel>
</IonTabButton>
</IonTabBar>
</IonTabs>
);
}

View File

@@ -0,0 +1,29 @@
.title {
margin-top: 0.35rem;
}
.customInput {
border-radius: 22px !important;
--padding-bottom: 1rem;
--padding-top: 1rem;
box-shadow: 0 0.7px 0.9px rgba(0, 0, 0, 0.101),
0 0.9px 2.5px rgba(0, 0, 0, 0.145),
0 2.5px 6px rgba(0, 0, 0, 0.189),
0 8px 20px rgba(0, 0, 0, 0.29);
}
.customInput {
margin-bottom: 1rem !important;
}
.saveButton {
--border-radius: 12px !important;
--padding-top: 1.75rem !important;
--padding-bottom: 1.75rem !important;
}

View File

@@ -0,0 +1,115 @@
import {
IonBackButton,
IonButton,
IonButtons,
IonCol,
IonContent,
IonGrid,
IonHeader,
IonIcon,
IonItem,
IonLabel,
IonPage,
IonRow,
IonSelect,
IonSelectOption,
IonTextarea,
IonTitle,
IonToolbar,
useIonRouter,
} from '@ionic/react';
import styles from './Add.module.scss';
import { checkmarkOutline } from 'ionicons/icons';
import { getCategories, getNotes } from '../store/Selectors';
import { CategoryStore, NoteStore } from '../store';
import { addNote } from '../store/NoteStore';
import { useState } from 'react';
const Add = (): React.JSX.Element => {
const categories = CategoryStore.useState(getCategories);
const notes = NoteStore.useState(getNotes);
const [noteCategory, setNoteCategory] = useState<number | false>(false);
const [noteContent, setNoteContent] = useState<string>('');
const router = useIonRouter();
const add = () => {
const note = {
id: notes.length + 1,
category_id: noteCategory,
note: noteContent,
complete: false,
};
addNote(note);
router.goBack();
};
return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<IonBackButton />
</IonButtons>
<IonTitle className={styles.title}>Add note</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent fullscreen>
<IonGrid className="ion-padding-top">
<IonRow>
<IonCol size="12" className="ion-padding-start">
<h1 className={styles.mainTitle}>Add a note</h1>
</IonCol>
</IonRow>
<IonRow>
<IonCol size="12">
<IonItem lines="none" className={styles.customInput}>
<IonLabel position="floating">Category</IonLabel>
<IonSelect
placeholder="Select..."
value={noteCategory}
onIonChange={(e) => setNoteCategory(e.target.value)}
>
{categories.map((category) => {
return (
<IonSelectOption value={category.id} key={category.id}>
{category.name}
</IonSelectOption>
);
})}
</IonSelect>
</IonItem>
</IonCol>
</IonRow>
<IonRow>
<IonCol size="12">
<IonItem lines="none" className={styles.customInput}>
<IonLabel position="floating">Note</IonLabel>
<IonTextarea
value={noteContent}
onIonChange={(e) => setNoteContent(e.target.value)}
placeholder="Enter note text here..."
/>
</IonItem>
</IonCol>
</IonRow>
<IonRow>
<IonCol size="12">
<IonButton expand="block" className={styles.saveButton} onClick={add}>
<IonIcon icon={checkmarkOutline} />
Save note
</IonButton>
</IonCol>
</IonRow>
</IonGrid>
</IonContent>
</IonPage>
);
};
export default Add;

View File

@@ -0,0 +1,130 @@
.heading {
color: var(--lighter-blue-color);
font-size: 1rem;
}
.mainTitle {
font-size: 2rem;
font-weight: 700;
}
.slideHeader {
margin: 0 !important;
padding: 0;
padding: 1.5rem !important;
padding-top: 0 !important;
}
.slideCount {
margin: 0 !important;
padding: 0 !important;
padding-left: 1.5rem !important;
padding-top: 1rem !important;
}
.slideCount h6 {
color: var(--light-blue-color);
}
.slideHeader h4 {
color: white;
margin: 0 !important;
padding: 0 !important;
text-align: left;
font-size: 1.75rem;
}
.categorySlider {
margin-top: -1.6rem;
ion-slide {
width: 60%;
margin-right: 30px;
}
ion-col {
padding-left: 0;
ion-card {
width: 100%;
border-radius: 22px;
// box-shadow: 0px 4px 12px 2px rgba(0, 0, 0, 0.16);
box-shadow: 0 0.7px 0.9px rgba(0, 0, 0, 0.101),
0 1.9px 2.5px rgba(0, 0, 0, 0.145),
0 4.5px 6px rgba(0, 0, 0, 0.189),
0 15px 20px rgba(0, 0, 0, 0.29);
ion-card-header {
text-align: left !important;
}
ion-card-content {
padding: 0;
padding: 0.5rem;
}
}
}
.lastUsed {
display: flex;
flex-direction: row;
align-content: flex-start;
align-items: center;
justify-content: center;
font-size: 0.9rem;
color: white;
}
.categoryColor {
border-bottom: 3px solid var(--pink-color);
margin-left: 1rem;
margin-right: 1rem;
margin-bottom: 0.5rem;
margin-top: -1rem;
}
}
.recentNotes {
padding-left: 0.5rem;
padding-right: 0.5rem;
max-height: 18.8rem;
overflow: scroll;
ion-item {
--padding-start: 2rem;
--padding-end: 2rem;
--border-style: none;
--border-radius: 22px;
--min-height: 4rem;
h4 {
padding-left: 1rem;
margin-top: 0.6rem;
font-size: 1.2rem;
font-weight: 400;
}
}
}
.bottomContainer {
margin-top: -1rem;
}

View File

@@ -0,0 +1,166 @@
import {
IonButton,
IonButtons,
IonCard,
IonCardContent,
IonCardHeader,
IonCardSubtitle,
IonCheckbox,
IonCol,
IonContent,
IonFab,
IonFabButton,
IonGrid,
IonHeader,
IonIcon,
IonItem,
IonPage,
IonRow,
// IonSlide,
// IonSlides,
IonToolbar,
} from '@ionic/react';
import styles from './Home.module.scss';
import { addOutline, menuOutline, notificationsOutline, searchOutline } from 'ionicons/icons';
import { getCategories, getNotes } from '../store/Selectors';
import { CategoryStore, NoteStore } from '../store';
import { markNote } from '../store/NoteStore';
const Home = (): React.JSX.Element => {
return <>TODO: need update IonSlide</>;
const categories = CategoryStore.useState(getCategories);
const notes = NoteStore.useState(getNotes);
const getNoteStyle = (categoryID, isComplete = false) => {
const categoryColor = categories.filter((category) => category.id === categoryID)[0].color;
return {
'--background': categoryColor,
'--background-checked': categoryColor,
'--border-style': 'none',
opacity: isComplete ? '0.6' : '1',
};
};
return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<IonButton>
<IonIcon icon={menuOutline} />
</IonButton>
</IonButtons>
<IonButtons slot="end">
<IonButton>
<IonIcon icon={searchOutline} />
</IonButton>
<IonButton>
<IonIcon icon={notificationsOutline} />
</IonButton>
</IonButtons>
</IonToolbar>
</IonHeader>
<IonContent fullscreen>
<IonGrid>
<IonRow>
<IonCol size="12" className="ion-padding-start">
<h1 className={styles.mainTitle}>Hello, Alan!</h1>
</IonCol>
</IonRow>
<IonRow>
<IonCol size="12" className="ion-padding-start ion-padding-top">
<IonCardSubtitle className={styles.heading}>Categories</IonCardSubtitle>
</IonCol>
</IonRow>
</IonGrid>
<IonSlides
id="slider"
options={{ slidesPerView: 'auto', zoom: true, grabCursor: true }}
className={`${styles.categorySlider} ion-padding-bottom`}
>
{categories.map((category, index) => {
const noteCount = notes.filter((n) => n.category_id === category.id).length;
return (
<IonSlide key={`categorySlide_${index}`}>
<IonCol className="ion-text-left">
<IonCard>
<IonCardHeader className="ion-no-padding">
<div className={styles.slideCount}>
<h6>
{noteCount} {noteCount === 1 ? 'note' : 'notes'}{' '}
</h6>
</div>
<div className={styles.slideHeader}>
<h4>{category.name}</h4>
</div>
</IonCardHeader>
<IonCardContent>
<div
className={styles.categoryColor}
style={{ borderBottom: `2px solid ${category.color}` }}
></div>
</IonCardContent>
</IonCard>
</IonCol>
</IonSlide>
);
})}
</IonSlides>
<IonGrid className={styles.bottomContainer}>
<IonRow>
<IonCol size="12" className="ion-padding-start">
<IonCardSubtitle className={styles.heading}>Recent Notes</IonCardSubtitle>
</IonCol>
</IonRow>
<div className={styles.recentNotes}>
{notes.map((note, index) => {
return (
<IonRow
key={`note_${index}`}
className="animate__animated animate__faster"
id={`noteRow_${note.id}`}
>
<IonCol size="12">
<IonItem>
<IonCheckbox
checked={note.complete}
style={getNoteStyle(note.category_id, note.complete)}
onClick={() => markNote(note.id)}
/>
<h4
style={
note.complete ? { textDecoration: 'line-through', opacity: '0.6' } : {}
}
>
{note.note}
</h4>
</IonItem>
</IonCol>
</IonRow>
);
})}
</div>
</IonGrid>
<IonFab vertical="bottom" horizontal="end" slot="fixed" className="ion-padding">
<IonFabButton routerLink="/add">
<IonIcon icon={addOutline} />
</IonFabButton>
</IonFab>
</IonContent>
</IonPage>
);
};
export default Home;

View File

@@ -0,0 +1,33 @@
import { Store } from 'pullstate';
interface Category {
id: number;
name: string;
count: string;
color: string;
}
const CategoryStore = new Store({
categories: [
{
id: 1,
name: 'Business',
count: '34',
color: '#60b660',
},
{
id: 2,
name: 'Personal',
count: '12',
color: '#1D68DF',
},
{
id: 3,
name: 'Leisure',
count: '23',
color: '#EB06FF',
},
] as Category[],
});
export default CategoryStore;

View File

@@ -0,0 +1,58 @@
import { Store } from 'pullstate';
interface Note {
id: number;
category_id: number;
note: string;
complete: boolean;
}
const NoteStore = new Store({
notes: [
{
id: 1,
category_id: 1,
note: 'Daily meeting with team',
complete: false,
},
{
id: 2,
category_id: 2,
note: 'Pay monthly rent',
complete: true,
},
{
id: 3,
category_id: 3,
note: 'Workout in the gym',
complete: false,
},
{
id: 4,
category_id: 1,
note: 'Make progress on project',
complete: false,
},
] as Note[],
});
export const markNote = (noteID: number) => {
const noteIndex = NoteStore.currentState.notes.findIndex((n) => n.id === noteID);
NoteStore.update((s) => {
s.notes[noteIndex].complete = !s.notes[noteIndex].complete;
});
document.getElementById(`noteRow_${noteID}`).classList.add('animate__pulse');
setTimeout(() => {
document.getElementById(`noteRow_${noteID}`).classList.remove('animate__pulse');
}, 500);
};
export const addNote = (note: Note) => {
NoteStore.update((s) => {
s.notes = [note, ...s.notes];
});
};
export default NoteStore;

View File

@@ -0,0 +1,16 @@
import { createSelector } from 'reselect';
import { Category, Note } from './NoteStore';
interface State {
categories: Category[];
notes: Note[];
}
const getState = (state: State) => state;
// General getters
export const getCategories = createSelector(getState, (state) => state.categories);
export const getNotes = createSelector(getState, (state) => state.notes);
// More specific getters
// export const getCoffee = (id: number) => createSelector(getState, state => state.coffees.filter(c => parseInt(c.id) === parseInt(id))[0]);

View File

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

View File

@@ -1,103 +0,0 @@
#about-page {
ion-toolbar {
position: absolute;
top: 0;
left: 0;
right: 0;
--background: transparent;
--color: white;
}
ion-toolbar ion-back-button,
ion-toolbar ion-button,
ion-toolbar ion-menu-button {
--color: white;
}
.about-header {
position: relative;
width: 100%;
height: 30%;
}
.about-header .about-image {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
background-position: center;
background-size: cover;
background-repeat: no-repeat;
opacity: 0;
transition: opacity 500ms ease-in-out;
}
.about-header .madison {
background-image: url('/assets/WeatherDemo/img/about/madison.jpg');
}
.about-header .austin {
background-image: url('/assets/WeatherDemo/img/about/austin.jpg');
}
.about-header .chicago {
background-image: url('/assets/WeatherDemo/img/about/chicago.jpg');
}
.about-header .seattle {
background-image: url('/assets/WeatherDemo/img/about/seattle.jpg');
}
.about-info {
position: relative;
margin-top: -10px;
border-radius: 10px;
background: var(--ion-background-color, #fff);
z-index: 2; // display rounded border above header image
}
.about-info h3 {
margin-top: 0;
}
.about-info ion-list {
padding-top: 0;
}
.about-info p {
line-height: 130%;
color: var(--ion-color-dark);
}
.about-info ion-icon {
margin-inline-end: 32px;
}
/*
* iOS Only
*/
.ios .about-info {
--ion-padding: 19px;
}
.ios .about-info h3 {
font-weight: 700;
}
}
#date-input-popover {
--offset-y: -var(--ion-safe-area-bottom);
--max-width: 90%;
--width: 336px;
}