🗓️

時間割表を作成するサイトを作ってみた

2021/12/02に公開

アプリのリンク↓
https://deizu.vercel.app/

React.Jsで時間割表を作成するサイト「DEIZU」を作ってみた。

DEIZU Screenshot
DEIZUの「真ん丸」というテーマを使用した状態

おしゃれな時間割表を作りたいなと思い、2020年の10月頃から作る予定を立ていました。最終的にはReact.Jsを学ばせてくれた良いプロジェクトだったと感じています。React初心者が作ったものにしてはスタイリッシュで良いものが最終的にできたのでは無いかと思います。

✏️ 現時点搭載せれている機能:

  • シンプルでユーザーフレンドリーなUI
  • Googleアカウントでログイン
  • 端末問わず使用可能(Web上)
  • 科目のセルの色、オンライン授業のリンク、教室名・場所等の情報書き込み可
  • 時間割表の時間区間を指定可能
  • 何枚もの時間割表を作成・保存・消去が可能
  • 画像URLで壁紙の指定が可能
  • テーマの変更が可能
  • UIの色が変更可能

DEIZU Dashboard
ユーザーがログインしたら見るダッシュボードの様子(画面の上に移っている黒い模様はプロフィール画像をぼかせたものです)

詳しい情報についてはDEIZUの紹介サイトを見てみて下さい!
DEIZUの紹介サイトはNext.JsでVercelを通してホスティングしました:
https://deizu.vercel.app/

実際のサイトはこちらです:
https://deizu-site.web.app/

ウェブアプリの方はFirebaseを使用しており、 Firebase Hostingを通し、ホスティングしました。
DEIZUを作る際にはReactの知識があまりなかったため、create-react-appから始め今に至ります。Next.Jsという存在を認識しなかったため、現在は紹介サイトがNext.Jsと実際のウェブアプリがCRAでできておりコードベースが共有されていない変な状態です。今の所問題は無いのですが、ウェブアプリの方も必要であればNext.Jsに移したい思っています。

🛠️ DEIZUの基本的な開発状況:

🏗️ コンポーネントストラクチャー

アプリのコンポーネントストラクチャー↓

App.js
├─ Dashboard.js
│  ├─ LoginEditor.js
│  │  ├─ NavItem.js
│  │  ├─ ToggleButton.js
│  │  ├─ DropdownSettings.js
│  │  │  ├─ DropdownItem.js
│  │  │  ├─ ThemeButton.js
│  │  │  ├─ ThemeColorButton.js
│  │  ├─ ScheduleGrid.js
│  │  │  ├─ ScheduleCell.js
│  │  │  ├─ TimeLabel.js

<DeizuButton/>というコンポーネントも有りますが、ほとんど全てのコンポーネントにあるので上のコンポーネントトリーには含めませんでした。

Reactのステートは全てApp.jsまたはDashboard.Jsに有り、propを通してデータが受け渡されています。

📝 Firestoreへの書き込み

Firestoreへのデータの書き込みはいくつの場所で行われます。
DEIZUを作る前、先ずどういったデータを保存する必要があるかを考えると、考えれば考えるほど思った以上に有りました。時間割表のタイトル、科目名、リンク、セルの色、セルの概要、時間等の情報が有りました。

これらの情報はアプリ全体に置いて異なるコンポーネントから書き込みが行われるため、各々の保存情報を(ばらつき無いよう)コンポーネント毎に分け、このようになりました:

コンポーネント Firestoreに保存する内容
<Dashboard /> 時間割表の名前・タイトル
<ScheduleCell /> 時間割表の科目名、リンク、セルの色、概要等の情報
<TimeLabel /> 時間(〇〇時〜〇〇時まで等の情報)
<DropdownSettings /> ユーザーが設定したテーマ・壁紙の情報

DEIZUを作るにあたってはFirestoreの使用に慣れていたため何も考えずFirestoreをデータベースとして開発し始めました。運良くFirestoreはコレクッション型データベースであっためDEIZUのニーズには完璧でした。

DEIZUユーザーのデータを保存するには「User」というコレクッションで、ユーザーに与えられるユニークなIDをタイトルとして持つドキュメントがあります。

Collection:Users

Documents:「ユーザーに与えられるユニークなID」

ユーザー各々が持つDocumentにはsheetsというオブジェクトがあり、その中には更に「シートのタイトル」のオブジェクトが有ります。シートのタイトルを持つオブジェクトには更に「cells」というオブジェクトがあり、セル各々のフィールドがあります。

DEIZU Firestore Screenshot
Firestore dbのスクリーンショット(「Hello」というタイトルの時間割表の例)

<ScheduleCell/>でのデータの保存方法:

const saveSubject = async (e) => {
	e.preventDefault();
	dataRef.doc(user.uid).set({
		sheets:{
			[sheetTitle]: {
				date: createdAt,
				cells: {
				[cellName]: {
					[cellName]: subjectName,
					[cellLink]: subjectLinkValue,
					[cellDscrp]: subjectDescription,
					[cellClr]: cellColor
					}
				}
			}
		}
	}, { merge: true })
	setIsOpen(false);
}

セルはクリックすると各々モーダルウィンドウが出てくるようにしたいため、コンポーネントにしました。しかし、セルは違ったFirestoreのフィールドにデータを書き込むため、propを通してセル各々にユニークな名前(a1,a2,a3...)を与える事ができます。

DEIZU Wireframe
※このスクリーンショットはDEIZUを実際作る前のFigma上のワイヤーフレームです。

こうすることで、propのバリューを使用し、セルコンポーネントの区別ができます。
時間割表のセルのデータは全て「シートのタイトル」のオブジェクトの下に入っているため、実際にデータを書き込むには「シートのタイトル」が必要であり、これもpropとして受け渡す事ができます。

<ScheduleCell/>prop達:

<ScheduleCell
	corner={cornerProps}
	selectorColor={selectorColorProps}
	cellId="a1"
	sTitle={sheetTitle}
/>

cornerselectorColorのプロパティーはテーマを変換する際に使われます(後々説明します)。

👀 時間割表のタイトルを表示

時間割表のタイトルは<Dashboard.js/><LoginEditor/>の内の<DropdownOthersheets/>のコンポーネント内で表示されます。基本的には両方とも同じなのですが、<DropdownOthersheets/>の中にあるものはタイトル以外に最終変更がされた日程が表示されています。

const getSheetTitles = () => {
	console.log("fetch");
	dataRef.doc(user.uid).get().then((doc) => {
		const titleForOtherSheets = doc.data().sheets;
		const arrayTitle = Object.keys(titleForOtherSheets);
		for (let i = 0; i < arrayTitle.length; i++) {
			const firestoreTime = Object.values(titleForOtherSheets)[i].date.toDate().toDateString();
			otherSheetsArray.push(arrayTitle[i] + '$' + firestoreTime);
		}
		setOtherSheets(otherSheetsArray);
	}).catch((error) => {
		console.log("Error getting document:", error);
	})
}

上にあるgetSheetTitles()の関数は、シートのタイトルと最終変更日をFirestoreから取得しotherSheetsArrayという空っぽなArrayに入れる。このArrayはReactのuseStatesetOtherSheets())でotherSheetsという変数に保存されます。

よって、otherSheetsは時間割表のタイトルといつ最終変更が行われたかの情報が$で挟まれたがArrayであるため、map()split()関数で各々の情報を表示することができます。
<DropdownOthersheets/>の中に<OtherSheet/>があります。

function OtherSheet() {
	let itemsToRender;
	if (otherSheets) {
		itemsToRender = otherSheets.map(item => {
		return <section
				className="dropdownItemSheet"
				key={item}
				onClick={() => {
					setTitleValue(`${(item.split('$')[0])}`);
					setOpenSheetsDropdown(false);
				}}>{item.split('$')[0]}
				<time>{item.split('$')[1]}</time>
			</section>;
			});
		} else {
			itemsToRender = <p style={{color:'var(--txtColor0'}}><h3>作成した時間割表はありません。</h3>作成した表が表示されない場合更新ボタンを押して下さい</p>;
      }
      return <>{itemsToRender}</>;
    }

📦 できるものは全てコンポーネントに!

Reactを使用するにあたって最初は、ボタンやコンテナーしかコンポーネントにできないと思っていたのですが、それは違って、わりとなんでもコンポーネントにできることを知りました。

例えばDEIZUのテーマや時間割表の切り替えをする時のドロップダウン。このドロップダウンは<ToggleButton>というコンポーネントで可能となっています。コンポーネントのpropとそれが何をするかがこちらです:

プロパティー 機能
btnTitle ボタンをカーソルにかざすと出てくるラベルの内容
btnIcon ボタンを押す前のアイコン(Material Iconを使用しました)
dropDownState ドロップダウンを操作するステート
btnClick ボタンをクリックした時の動作
dropDownComponent ボタンをクリックした時に出てくるドロップダウンコンポーネント

他の表を表示するコンポーネントはこんな感じです:

<ToggleButton
	btnTitle="他の表"
	btnIcon={<MdList />}
	dropDownState={openSheetsDropdown}
	btnClick={(e) => {
			e.preventDefault();
			setOpenSettingsDropdown(false);
			setOpenSheetsDropdown(!openSheetsDropdown);
			getSheetTitles();
		}
	}
	dropDownComponent={<DropdownOthersheets/>}
/>

設定のドロップダウンはドロップダウンのコンポーネントの中にさらに段階がふまれており、これを作成する際にはFireship.ioさんにあるチュートリアルを参考に作成しました。コンポーネントをpropとして受け渡すことができるのを知ったのもこちらのチュートリアルの動画のおかげです:
https://fireship.io/lessons/dropdown-menu-multi-level-react/

Fireship.ioのチュートリアルは非常にわかりやすく端的と説明してくれるので最高です👍

✨ 見た目の変更(説明・やり方)

少しでもユーザーにカスタマイズできる自由があると良いと思ったので、DEIZUには色の切り替え・テーマの切り替えができる設定を入れました。色とテーマの切り替えは両方同じ方法を用い行いました:

1. 先ず切り替えたい見た目をCSS Variableとして保存:

色テーマの切り替えは6つの異なるCSS Variableによって指定されており、DEIZUの場合だとそれらは:

CSS Variable名 デフォルトで指定した色
system0 white
system1 #ececec
system2 #dbdbdb
system3 black
txtColor0 black
txtColor1 white

2. 切り替えをすばやくできるPresetを作成:

「1」で示した手順はデフォルトの状態であるため、色の切り替えを可能にするにはユーザーが切り替えれるオプションの色のコンビネーションも作成する必要があります。

DEIZUの場合だと、色の異なるコンビネーションはsrc/theme-data/themeColors.jsにObject Arrayとして保存されています。

// 色名:[system0, system1, system2, system3, txtColor0, txtColor1]

export const Colors = {
    default: ['white','#ececec','#dbdbdb','black','black','white'],
    light: ['white','#f5f5f5','#f3f3f3','#e2e2e2','black','black'],
    yellowGrey: ['#ffe294','#ffd666','#ffc933','#ffba01','black','white'],
    greyBlack: ['#757575','#616161','#424242','#212121','black','white'],
    brown: ['#6D4C41','#5D4037','#4E342E','#3E2723','white','white'],
    blueWhite: ['#bae1ff','#7cc6fe','#47afff','#4ba3c3','#dssera','white'],
    lightGreen: ['#AED581','#9CCC65','#8BC34A','#7CB342','black','black'],
    deepOrange: ['#FFAB91','#FF8A65','#FF7043','#FF5722','black','black'],
    blueGrey: ['#546E7A','#455A64','#37474F','#263238','white','white'],
    deepPurple: ['#7E57C2','#673AB7','#5E35B1','#512DA8','white','white']
}

各々のオブジェクトはの名前はテーマの名前であり、Arrayに保存されている順番は[system0,system1,system2,system3,txtColor0,txtColor1]である。

3. JavaScriptからCSS Variableにアクセス:

色の切り替えが反映されたい場所から、新しいuseStateを作成する。このステートはユーザーが現在設定しているテーマの情報をアプリ内で保存する場となる(DEIZUの場合だと<Dashboard/>のコンポーネントに書かれています)。

今回はデフォルトの色で設定したバリューを入れる:

const [dashSystemColorStyle, setDashSystemColorStyle] = useState([
	'white',
	'#ececec',
	'#dbdbdb',
	'black',
	'black',
	'white',
]);

次にuseEffectフックで上のステートで設定したバリューをCSS Variableと継げる(ReactとCSS Variableに関する情報はこちらの記事を参考にさせていただきました):

useEffect(() => {
	root?.style.setProperty("--system0", dashSystemColorStyle[0]);
	root?.style.setProperty("--system1", dashSystemColorStyle[1]);
	root?.style.setProperty("--system2", dashSystemColorStyle[2]);
	root?.style.setProperty("--system3", dashSystemColorStyle[3]);
	root?.style.setProperty("--txtColor0", dashSystemColorStyle[4]);
	root?.style.setProperty("--txtColor1", dashSystemColorStyle[5]);
})

4. Firestoreに情報を書き込む・受け取る

Firestoreに書き込む

実際のアプリケーションではテーマのボタンを押し、ページをリロードすると、変更が見られる状態となっています。(テーマのボタンを<ThemeColorButton />のコンポーネント)
「#2」で作成した、切り替えをすばやくできるPresetを用いて、propを通し現在のテーマをsystemColorStyleuseStateで保存する。

const [systemColorStyle, setSystemColorStyle] = useState(props.systemColorStyle);

ボタンのonclickイベントに以下のような関数を入れることで選択したテーマを保存することができます。

const saveThemeColor = () => {
	dataRef.doc(user.uid).set({
		themeColor: systemColorStyle
	}, { merge: true });
}

Firestoreから設定したテーマの情報を受け取る

<Dashboard/>のコンポーネントに戻って、ステートの変化に応じて起動するuseEffectフックでFirstoreから情報を受け取ることができます。

useEffect(() => {
	dataRef.doc(user.uid).get().then((doc) => {
		const themeColorData = doc.data().themeColor;
		setDashSystemColorStyle([
			themeColorData[0],
			themeColorData[1],
			themeColorData[2],
			themeColorData[3],
			themeColorData[4],
			themeColorData[5],
		]);
	}).catch((error) => {
		console.log("Error getting document:", error);
	});
},[])

これでテーマをFirestoreのデータベースに保存・読み込む事ができました🎉(ページをリロードすることでテーマの変更が見られます)

🎯 まとめ

今まで作ったアプリケーションは、遊びや学校の為と言ったものだったので、ウェブアプリを一般公開するのは初めてです。既にリリースされているのですが、バグ等はまだたくさんあるかも知れません、、、
少しでも興味を持ってくれればアカウント登録でもして使ってみて下さい♪

Discussion