LoginSignup
164
196

More than 3 years have passed since last update.

初心者がReact & Firebaseを使って収支管理アプリを作成(解説編)

Last updated at Posted at 2020-10-10

今回作ったもの

ログイン機能付きの収支管理アプリです。
いわゆる家計簿アプリ的な物です。
毎月の収入と支出をリスト化し月の残高を表示します。

【操作動画】 
*GIFなので画質悪くてすいません。
GIF.gif

感想編については、別途記事を書いてるので、ご興味ございましたら是非ご覧下さいませ!
初心者がReact学習歴1週間でWebアプリ作成に挑戦してみた

コードについて

長くなってしまうので全部は載せておりません。
必要だと思うところだけ解説してます。
スタイルを基本CSSで装飾してますが、classNameは邪魔だと思うのでこの記事では消してます。
全コードはGithubに載せてます。
こちら

使用技術

  • React(version 16.13.1)
    • Router
    • クラスの代わりにHooks (useState, useEffect, useContext)
  • Firebase
    • Authentication
    • Cloud Firestore
    • Hosting

Reactの準備

create react appで作成

npx create-react-app my-app
cd my-app
npm start

Firebaseの準備

プロジェクトを作成

スクリーンショット 2020-10-05 午後3.47.11.png

作成方法は手順に従えば大丈夫です。
簡単なので割愛しますが、全体の流れはこちらのYoutubeを参考にしました。
日本一わかりやすいReact入門【実践編】#3...Firebaseプロジェクトの作成と初めてのデプロイ

注意点として、node.jsとfirebaseのバージョンにより少し挙動が動画内容と異なります。

ポイントは、

  • ロケーション設定は初めにやる。東京なら「asia-northeast1」
  • プランは現在の安定node versionだとSparkではなくBlazeプラン(従量制)になります
  • Blazeプランなので、念の為Google Cloud Platformを作成して、アクセス& 料金状況/アラーム通知を受信できるよう設定します(任意)

Authenticationの設定

Sign-in methodよりステータスを有効にする
今回は「メール/パスワード」を使用
signinmethod.png

Cloud Firestoreでデータベース作成

  • テストモードで実行
  • とりあえずコレクション/ドキュメントを追加してみる(イメージのため)

最終的なコレクション/ドキュメント構成はこちらです。

スクリーンショット 2020-10-05 午後3.50.32.png

Firebaseの設定

Firebaseの情報をwebアプリに登録します。
プロジェクト内容は一応セキュリティを考慮し.envに登録します。
.envgitignoreすれば公開される心配がないということですね。

REACT_APP_FIREBASE_KEY="APIキー"
REACT_APP_FIREBASE_DOMAIN="プロジェクト.firebaseapp.com"
REACT_APP_FIREBASE_DATABASE="https://プロジェクト.firebaseio.com"
REACT_APP_FIREBASE_PROJECT_ID="プロジェクト"
REACT_APP_FIREBASE_STORAGE_BUCKET="プロジェクト.appspot.com"
REACT_APP_FIREBASE_SENDER_ID="ID番号"

srcディレクトリ直下にfirebaseディレクトリとFirebase.jsファイルを作成します。
ここでFirebaseの初期化処理が行われます。
authdbも作り、毎回全部書かなくて済むようにします。

src/firebase/Firebase.js

import firebase from "firebase/app";
import "firebase/auth";
import "firebase/firestore";

firebase.initializeApp({
  apiKey: process.env.REACT_APP_FIREBASE_KEY,
  authDomain: process.env.REACT_APP_FIREBASE_DOMAIN,
  databaseURL: process.env.REACT_APP_FIREBASE_DATABASE,
  projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID,
  storageBucket: process.env.REACT_APP_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: process.env.REACT_APP_FIREBASE_SENDER_ID
});

const auth = firebase.auth();
const db = firebase.firestore();

export { auth, db }

ログイン機能の実装

authディレクトリを作成し、こちらに認証機能系は集約させます。

ディレクトリ構成

src
 ├── auth
    ├── AuthProvider.js
    └── Login.js
    └── PrivateRoute.js
    └── SignUp.js
 ├── components
 ├── firebase
 ├── App.js

まず、App.jsにログイン状態で表示ページを変える為、Routerを作ります。
exactはpathが「含む」とならないように指定。

src/App.js
const App = () => {
  return (
    <AuthProvider>
      <Router>
        <Switch>
          <PrivateRoute exact path="/" component={Home} />
          <Route exact path="/login" component={Login} />
          <Route exact path="/signup" component={SignUp} />
        </Switch>
      </Router>
    </AuthProvider>
  );
};

export default App;

AuthPrivider.js

認証の情報(ユーザーがログイン、サインアップする)は、こちらで作ります。
そしてユーザー情報が必要なコンポーネントでuseContextを使います。
通常データはトップダウン形式でpropsを渡さないといけないですが、
contextを使うことで、コンポーネントツリーに簡単にデータを共有することができます。

useContextについてこちらの記事が非常にわかりやすかったです。
useContextの使い方
こんなに簡単なの?React useContextって

src/auth/AuthProvider.js
import React, { useEffect, useState } from "react";
import { auth } from "../firebase/Firebase";

const AuthContext = React.createContext() 

const AuthProvider = ({ children }) => {
  const [currentUser, setCurrentUser] = useState(null);

  //サインアップ後認証情報を更新
  const signup = async (email, password, history) => {
    try { 
      await auth.createUserWithEmailAndPassword(email, password);
      auth.onAuthStateChanged(user => setCurrentUser(user));
      history.push("/");
    } catch (error) {
      alert(error);
    }
  };

  //ログインさせる
  const login = async (email, password, history) => {
    try {
      await auth.signInWithEmailAndPassword(email,password);
      auth.onAuthStateChanged(user => setCurrentUser(user));
      history.push("/");
    } catch (error) {
      alert(error);
    }
  }

  //初回アクセス時に認証済みかチェック
  useEffect(() => {
    auth.onAuthStateChanged(setCurrentUser);
  }, []);

  return (
    <AuthContext.Provider value={{ signup, login, currentUser}}>
      {children}
    </AuthContext.Provider>
  )
}

export {AuthContext, AuthProvider}

初期値のユーザーのステートはnullですね。

signup関数
引数にemailpasswordhistoryを渡して非同期処理を行います。
auth.createUserWithEmailAndPassword(email, password)は、
firebaseのメソッドでemailpasswordを元にアカウントが作成されます。
その後userの情報を取得し、CurrentUserにセットします。

history.push("/")は、reactRouterの画面遷移させる機能です。
今回はログイン後、Home画面に行きます。

login関数
同じ様に、ユーザーがログインしたら情報を取得しCurrentUserを更新するようにします。
auth.signInWithEmailAndPassword(email,password)
これもまたfirebaseのメソッドです。

あとは、最初にログインしてるか確認する為、useEffectで一回だけauth.onAuthStateChangedを実行します。
※一回だけ実行したいので、第二引数には空の配列[]を渡します。

PrivateRoute.js

アプリのメイン画面Home.jsは、PrivateRouteに指定します。
ここで、ユーザーがログインしてれば→メイン画面を表示。
未ログインの場合→ログイン画面を表示。の作業を行なってます。

src/auth/PraveteRoute.js
import React, { useContext } from "react";
import { Route } from "react-router-dom";
import { AuthContext } from "./AuthProvider";
import Login from "./Login";

const PrivateRoute = ({ component, ...rest}) => {
  const { currentUser } = useContext(AuthContext);
  //AuthContextからcurrentUserを受け取る

  const Component = currentUser ? component : Login;
  //currentUserがtrueの場合component=Home、falseならLoginコンポーネントにroute

  return <Route {...rest} component={Component} />;
};

export default PrivateRoute;

...restは、残りのpropsをまとめて引数に渡してます(今回他のpropsはないですが)
この...ですが、以前RestParametersの記事を書きましたのでよろしければご参照ください。
ES6の新しい構文です!
スプレッド構文とRestパラメータを理解する

SignUp.jsとLogin.js

SignUp.jsコンポーネントでは、ユーザー登録画面の表示、登録内容を取得します。
handleSubmitが実行される時、入力されたemailpasswordの内容をAuthProviderで作ったsignup関数の引数に渡してデータが登録されます。
アップデート後のhistory(情報)を渡すために、最後exportの時withRouter(SignUp)を使っています。

src/auth/SignUp.js
const SignUp = ({ history }) => {
  const { signup } = useContext(AuthContext);
  //AuthContextからsignup関数を受け取る

  const handleSubmit = event => {
    event.preventDefault();
    const { email, password } = event.target.elements;
    signup(email.value, password.value, history);
  };

  return (
      <div>
        <h1>Sign Up</h1>
        <form onSubmit={handleSubmit}>
          <div>
            <label>E-mail Address</label>
            <input name="email" type="email" placeholder="email@gmail.com" />
          </div>
          <div>
            <label>Password</label>
            <input name="password" type="password" placeholder="Password"/>
          </div>
            <SignUpButton type="submit">SIGN UP</SignUpButton>
        </form>
        <Link to="/login">SignInへ戻る</Link>
      </div>
    </div>
  );
};

export default withRouter(SignUp);

Login.jsも似たような感じで作ります。
signup部分をloginに変えるだけですね。

src/auth/Login.js
const Login = ({ history }) => {
  const { login } = useContext(AuthContext);
  //AuthContextからlogin関数を受け取る

  const handleSubmit = event => {
    event.preventDefault();
    const { email, password } = event.target.elements;
    login(email.value, password.value, history);
  };

コンポーネント・ファイル構成

続いてメイン画面です。
アプリのメイン画面を担うコンポーネント達がこちら。
メイン(親)のコンポーネントは、Home.jsになります。

src
 ├── auth
 ├── components
    ├── Home.js
    └── Header.js
    └── Balance.js
    └── IncomeExpense.js
    └── AddItem.js
    └── ItemsLists.js
    └── IncomeItem.js
    └── ExpenseItems.js
    └── TotalIncomeExpense.js //共通関数ファイル
 ├── firebase
 ├── App.js

画面上ではこんな感じ。
main.png

ステートの作成/更新

親コンポーネントから子コンポーネントに渡す為、ステートは全てHome.jsで作成します。

作成したステート達

src/compoments/Home.js
  const [inputText, setInputText] = useState("");
  const [inputAmount, setInputAmount] = useState(0);
  const [incomeItems, setIncomeItems] = useState([]);
  const [expenseItems, setExpenseItems] = useState([]);
  const [type, setType] = useState("inc");
  const [date, setDate] = useState(new Date());

収入incomeの配列incomeItemsと、支出expenseの配列expenseItemsは、後々計算が楽なので分けて作成。

データを追加する

Firestoreからデータを取得・追加は全てHome.jsで行います。

こちらは追加バージョン

src/compoments/Home.js
  const addIncome = (text, amount) => {
    const docId = Math.random().toString(32).substring(2);
    const date = firebase.firestore.Timestamp.now();
    db.collection('incomeItems').doc(docId).set({
      uid: currentUser.uid,
      text,
      amount,
      date,
    });
    .then(response => {
      setIncomeItems([
        ...incomeItems, {text: inputText, amount: inputAmount, docId: docId , date: date}
      ]); 
    });
  }
  • 収入income用の関数addIncomeを用意。引数にはユーザーが入力したtextamountを渡す。
  • docIdをこちらで作る。
  • 収入リストが順番に並べられるようにdateを作成。firebase.firestore.Timestamp.now()(入力時間が登録される)→firebaseのメソッド
  • どこのcollectionのdocumentに追加するかは、firebaseのメソッドを使用db.collection('incomeItems').doc(docId).set({})
  • setしたいデータを配列として追加uid ~ date
  • その後.thenで、reactアプリ側のsetIncomeItemsのステートを更新

ポイントは、
ユーザーが削除ボタンを押した時、データを削除するのにdocIdを使います。
reactアプリと連動させたいので、こちら側で手動で作成してます。
※その時、数値だとエラーになるので文字列に変換.toString(32).substring(2)
手動で作らない場合は、「.set」ではなく「.add」で自動生成可能。

これと同じ内容で出費expense用の関数も用意すれば、値はFirestoreとReact上で無事追加/更新されます。

データを取得する

Firestoreからデータを取ってきて、アプリ上で表示させます。

src/compoments/Home.js
  const getIncomeData = () => {
    const incomeData = db.collection('incomeItems')
    incomeData.where('uid', '==', currentUser.uid).orderBy('date').startAt(startOfMonth(date)).endAt(endOfMonth(date)).onSnapshot(query => {
      const incomeItems = []
      query.forEach(doc => incomeItems.push({...doc.data(), docId: doc.id}));
      setIncomeItems(incomeItems);
    });
  }
  • 取得したいデータのIncome用の関数getIncomeDataを作成
  • コレクションincomeItemsのドキュメントを取得→変数incomeDataに代入
  • uidが現在のユーザーと一致する場合のstartOfMonth~endOfMonthのドキュメントを取得
  • 取得したデータを保存する空の配列incomeItemsを作成
  • その配列にドキュメントのdataidpush(追加)する
  • reactアプリ側のincomeItemsの配列を更新する

ポイントは、
orderByのメソッドでdateを昇順にしてます。
リストがバラバラに表示されてしまうので、制御する為に必要です。
また、orderByで昇順にしようとするとfirebaseから「indexを作れ」というエラーが出ます。
その際、親切にURLが表示されるのでそこにアクセスしてindexを作ればOKです(結構時間かかります)
参考記事:複合index

startAtendAtは、その月の分だけ表示させる為です。
この引数の中身については、後ほど詳細を書きます。

これと同じ内容で出費expense用の関数も用意すれば、無事Firestoreからデータを取得できて、Reactのステートに更新/表示がされます。

尚、データを取得するタイミングは、useEffectを使って操作します。

  1. 最初の1回のみ実行してほしい。引数は空の配列。
  2. dateが更新されるたびに実行してほしい。※dateは次で解説してますがヘッダーの月の部分です。
src/compoments/Home.js
  useEffect (() => {
    getIncomeData();
    getExpenseData();
  }, []);

  useEffect(() => {
    getIncomeData();
    getExpenseData();
  }, [date]);

月ごとにデータを表示させる

月ごとにデータを分けて表示させる為、ステートで作ったdateを使います。
初期値は現在の日時が入ってます。

src/compoments/Home.js
const [date, setDate] = useState(new Date());

ヘッダーに渡して表示

dateは、ユーザーがヘッダーの"前月"か"次月"ボタンを押すと更新されます。
↓の関数で月の部分を変えてます。

src/compoments/Home.js
  //for Header
  const setPrevMonth = () => {
    const year = date.getFullYear();
    const month = date.getMonth()-1;
    const day = date.getDate();
    setDate(new Date(year, month, day));
  }

  const setNextMonth = () => {
    const year = date.getFullYear();
    const month = date.getMonth()+1;
    const day = date.getDate();
    setDate(new Date(year, month, day));
  }

これらはHeader.jsで使うのでpropsで渡してあげます。

src/compoments/Home.js
<Header 
  date={date}
  setPrevMonth={setPrevMonth}
  setNextMonth={setNextMonth}
/>

Header.jsでは、現在の月を表示させる為、yearmonthを作り、
隣に前月と次月ボタンを表示させます。

src/compoments/Header.js
  const today = date;
  const year = today.getFullYear();
  const month = today.getMonth()+1;

  return (
    <div className="head">
      <SignOutButton onClick={() => auth.signOut()}>Sign Out</SignOutButton>
      <div>
        <button onClick={() => setPrevMonth()}>前月 </button>
        <h1>{year}{month}</h1>
        <button onClick={() => setNextMonth()}> 次月</button>
      </div>
    </div>
  )

これでボタンをクリックしたら、setPrevMonth()setNextMonth()が実行され、月の表示が変わります。

ユーザーの入力内容を操作する

ユーザーが入力した内容を取得する関数は、AddItem.jsコンポーネントで行ってます。

src/components/AddItem.js
export const AddItem = ({ addIncome, addExpense, inputText, setInputText, inputAmount, setInputAmount, type, setType, selectedMonth, thisMonth}) => {

  const typeHandler = (e) => {
    setType(e.target.value);
  }

  const inputTextHandler = (e) => {
    setInputText(e.target.value);
  };

  const inputAmountHandler = (e) => {
    setInputAmount(parseInt(e.target.value));
  }

  const reset = () => {
    setInputText("");
    setInputAmount("");
  }

  const submitItemHandler = (e) => {
    e.preventDefault();
    if (inputText == '' || inputAmount == '0' || !(inputAmount > 0 && inputAmount <= 10000000)) {
      alert ('正しい内容を入力してください')
    } else if ( type === 'inc') {
      addIncome(inputText, inputAmount) 
      reset();
    } else if ( type === 'exp' ) {
      addExpense(inputText, inputAmount)
      reset();
    }
  }

  const thisMonthForm = () => {
    return (
      <form>
        <select onChange={typeHandler}>
          <option value="inc">+</option>
          <option value="exp">-</option>
        </select>
        <div>
          <label>内容</label>
          <input type="text" value={inputText} onChange={inputTextHandler}/>
        </div>
        <div>
          <label>金額</label>
          <input type="number" value={inputAmount} onChange={inputAmountHandler}/>
          <div></div>
        </div>
        <div>
        <AddButton type="submit" onClick={submitItemHandler}>追加</AddButton>
        </div>
      </form> 
    )
  }

  const otherMonthForm = () => {
    return (
      <form></form>
    )
  }

  return (
    <>
    {thisMonth === selectedMonth ? thisMonthForm() : otherMonthForm()}
    </>
  )

} 
  • 収入income、出費expense、どちらに追加するのかはtypeで分けてます。
  • ユーザーが、option を選択した時にtypeHandler関数でtypeの値を更新します。
  • inputの値とamountの値もonChangeで取得します。
  • amountについては、後に計算するのでparseIntで値を数値化します。

submitItemHandlerで追加ボタンが押された時、操作してる内容はこちら。

  • デフォルトのイベント(動作)をキャンセル
  • 正しい内容が入力されてない場合、エラーを表示
  • incタイプなら、Home.jsで定義したaddIncomeの引数に ユーザーの入力内容inputTextinputAmountを渡す
  • expタイプも同様

→Firestoreのデータが追加され、reactアプリのステートも更新されるという流れ。

ヘッダーが今月なのか、今月でない月かによって表示方法を変える為、条件付きレンダーを行ってます。
(※今月のみ追加フォームを表示させる為)

この条件に使ってるselectedMonththisMonthは、
他のコンポーネント(リスト)でも使うので、Home.jsで作ってpropsで渡してます。

src/components/Home.js
  //operate add form and income/expense list
  const selectedMonth = date.getMonth() + 1;
  const today = new Date();
  const thisMonth = today.getMonth() + 1;

リストの表示

リストの表示はItemsList.jsを作り、そこでitemsに対してmapを行い、IncomeItemExpenseItemをそれぞれ表示します。

src/components/ItemsList.js
export const ItemsList = ({ deleteIncome, deleteExpense, incomeItems, expenseItems, incomeTotal, selectedMonth, thisMonth}) => {

  return (
    <div>
      <div>
        <h3>収入一覧</h3>
          <ul>
            {incomeItems.map((incomeItem) => (
              <IncomeItem 
                deleteIncome={deleteIncome}
                incomeText={incomeItem.text}
                incomeAmount={incomeItem.amount}
                incomeItem={incomeItem}
                key={incomeItem.docId}
                selectedMonth={selectedMonth}
                thisMonth={thisMonth}
              />
            ))}
          </ul>
      </div>
      <div>
        <h3>支出一覧</h3>
        <ul>
            {expenseItems.map((expenseItem) => (
              <ExpenseItem
                deleteExpense={deleteExpense}
                expenseText={expenseItem.text}
                expenseAmount={expenseItem.amount}
                expenseItem={expenseItem}
                key={expenseItem.docId}
                incomeTotal={incomeTotal}
                selectedMonth={selectedMonth}
                thisMonth={thisMonth}
              />
            ))}
          </ul>
      </div>
    </div>
  )
}

mapを実行して作られた一つ一つの項目を表示されるコンポーネントがこちら↓

Incomeバージョン

src/components/IncomeItem.js
export const IncomeItem = ({ deleteIncome, incomeItem, incomeText, incomeAmount, thisMonth, selectedMonth}) => {

  const deleteHandler = () => {
    deleteIncome(incomeItem.docId);
  }

  const showThisMonth = () => {
    return (
      <li>
      <div>{incomeText}</div>
      <div>+{Number(incomeAmount).toLocaleString()}</div>
      <button onClick={deleteHandler}>×</button>
      </li>
    )
  }

  const showPastMonth = () => {
    return (
      <li>
      <div>{incomeText}</div>
      <div>+{Number(incomeAmount).toLocaleString()}</div>
      </li>
    )
  }

  return (
    <>
      {thisMonth === selectedMonth ? showThisMonth() : showPastMonth()}
    </>
  )
}

Number(incomeAmount).toLocaleString()は、カンマ「,」を表示させる為。
deleteHandlerは "×" を押した時にdleteIncomeを実行してます。
このdelteIncomeは、fireStoreのdocIdの関係上、Home.jsで定義されてます。↓
引数にこのincomeItem.docIdを渡せば該当のアイテムは削除されます。

src/compoments/Home.js
  const deleteIncome = (docId) => {
    db.collection('incomeItems').doc(docId).delete();
  }

firebaseのメソットを使い、incomeItemsコレクションにある、該当ドキュメントを削除してます。

あとは、スタイル上、表示の仕方を変えたいので、ここでも条件付きレンダーを行ってます。
→ showThisMonthshowPastMonthで分ける。
これで今月以外、削除ボタンを表示させないようにしてます。

これと同じようにexpenseバージョンも作ればOK

収入/支出の値を計算する

収入と支出の各合計

値の計算はそれぞれのincomeItemsexpenseItemsの配列から、amountを取り出して計算します。

src/components/IncomeExpense.js
export const IncomeExpense = ({ incomeTotal, expenseItems }) => {

  const expenseAmounts = expenseItems.map(expenseItem => expenseItem.amount);

  const expenseTotal = expenseAmounts.reduce((acc, cur) => acc += cur, 0);

  const percentage = () => {
    if (incomeTotal >= 1) {
      return `${Math.round((expenseTotal / incomeTotal) * 100)} %`;
    } else {
      return '---';
    }
  };

  return (
    <div>
      <div>
        <h2>収入</h2>
        <div>
          <p>+ {Number(incomeTotal).toLocaleString()}<span> </span></p>
      </div>
        </div>
      <div>
        <h2>支出</h2>
        <div>
          <p>- {Number(expenseTotal).toLocaleString()}<span> </span></p>
          <div>{percentage()}</div>
        </div>
      </div>
    </div>
  )
}

mapreduceについては、こちらを元に別で記事を書いています。
filterは途中色々変えたので、結局今回のアプリに使っていません。

map・filter・reduceの書き方・使い方

支出expenseの合計計算
- expenseAmountexpenseItemsの中のamountだけ取り出した配列を代入します。
- expenseAmountを使って、累計を計算し、expenseTotalとします。

これを収入incomeItemsでも同じことをします。
ただincomeバージョンについては、他のコンポーネントでも使うので、共通関数ファイルTotalIncome.jsを作成してます。

TotalIncome.jsで関数totalCalcを作り、Home.jsincomeItemsを引数に渡してます。

src/components/Home.js
  // calculate % and show total
  const incomeTotal = totalCalc(incomeItems);
src/components/TotalIncome.js
export const totalCalc = (incomeItems) => {
  const incomeAmounts = incomeItems.map(incomeItem => incomeItem.amount);
  return incomeAmounts.reduce((acc, cur) => acc += cur, 0);
};

こちらを使う他のコンポーネントとは、
割合%を表示するIncomeExpense.jsExpenseItemと、計算に必要なBalance.jsになります。

残高の計算

総合計の残高を計算をするBalance.jsコンポーネントでは、
IncomeTotal - ExpenseTotalをすれば計算できます。

src/components/Balance.js
const balance = incomeTotal - expenseTotal

アプリを公開

あとは公開するだけです!
私の場合はfirebase logininitは先に済ませてました。

> firebase login
> firebase init
- 選択項目は FireStore, Functions, Hosting
- publicディレクトリはbuildにする
> npm run build
> firebase deploy

あとがき

今回のアプリは全てデータをpropsで親から子供に渡しているので、
どうしてもメインのHome.jsが少しボリューミーになってしまいました。(そういうものなのか?)
初心者が書いたコードですので、ご理解いただければと思います。
こうした方が良い等ありましたら、是非ご指摘お願いします!
日々勉強して、もっと良い書き方でコードを書けるよう頑張ります。

参考

164
196
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
164
196