From a66d91d4eb737ff6d3e98dcacaef9bf5c2228c23 Mon Sep 17 00:00:00 2001 From: 273* Date: Sat, 9 May 2026 01:54:48 +0900 Subject: [PATCH 01/23] =?UTF-8?q?chore:=20todo=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(journal)/[id].tsx | 2 ++ src/app/(journal)/entry/[id].tsx | 1 + src/app/(journal)/index.tsx | 1 + 3 files changed, 4 insertions(+) diff --git a/src/app/(journal)/[id].tsx b/src/app/(journal)/[id].tsx index 4d2ecc6..5553a76 100644 --- a/src/app/(journal)/[id].tsx +++ b/src/app/(journal)/[id].tsx @@ -144,6 +144,8 @@ export default function JournalScreen() { ], }} /> + {/* TODO: 空の場合の処理を追加する */} + {/* エントリー一覧にはheaderTitleのデータは含まないので src/app/(journal)/entry/[id].tsx と混同しないように。 */} ); diff --git a/src/app/(journal)/entry/[id].tsx b/src/app/(journal)/entry/[id].tsx index d0d713d..4955a62 100644 --- a/src/app/(journal)/entry/[id].tsx +++ b/src/app/(journal)/entry/[id].tsx @@ -64,6 +64,7 @@ export default function EntryDetailScreen() { ], }} /> + {/* TODO: 空の場合の処理を追加する */} {entry && } ); diff --git a/src/app/(journal)/index.tsx b/src/app/(journal)/index.tsx index 3c08a97..4de32e6 100644 --- a/src/app/(journal)/index.tsx +++ b/src/app/(journal)/index.tsx @@ -27,6 +27,7 @@ export default function JournalListScreen() { ), }} /> + {/* TODO: 空の場合の処理を追加する */} ); From 4257ae27b68da888c83fbcfac164e99858da0462 Mon Sep 17 00:00:00 2001 From: 273* Date: Sun, 10 May 2026 11:30:03 +0900 Subject: [PATCH 02/23] =?UTF-8?q?fix:=20=E8=BB=BD=E5=BE=AE=E3=81=AA?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(journal)/[id].tsx | 7 ++++++- src/components/entry/entry-list-view.tsx | 7 ++++++- src/utils/journal/use-journal-field.ts | 3 +++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/app/(journal)/[id].tsx b/src/app/(journal)/[id].tsx index 5553a76..f9a654d 100644 --- a/src/app/(journal)/[id].tsx +++ b/src/app/(journal)/[id].tsx @@ -146,7 +146,12 @@ export default function JournalScreen() { /> {/* TODO: 空の場合の処理を追加する */} {/* エントリー一覧にはheaderTitleのデータは含まないので src/app/(journal)/entry/[id].tsx と混同しないように。 */} - + ); } diff --git a/src/components/entry/entry-list-view.tsx b/src/components/entry/entry-list-view.tsx index fe20435..8f2c58c 100644 --- a/src/components/entry/entry-list-view.tsx +++ b/src/components/entry/entry-list-view.tsx @@ -53,6 +53,8 @@ function groupByMonth( type Props = { /** ジャーナル id */ id: string; + /** ジャーナル */ + journalName: string; /** 検索テキスト */ searchText?: string; /** ソートキー */ @@ -64,6 +66,7 @@ type Props = { */ export function EntryListView({ id, + journalName, searchText = "", sortKey = "dateDesc", }: Props) { @@ -114,7 +117,9 @@ export function EntryListView({ router.push("/(journal)/create")} + onPress={() => + router.push(`/(journal)/entry/create?name=${journalName}`) + } style={styles.fab} > Date: Sun, 10 May 2026 11:46:56 +0900 Subject: [PATCH 03/23] =?UTF-8?q?fix:=20=E3=82=A8=E3=83=B3=E3=83=88?= =?UTF-8?q?=E3=83=AA=E3=83=BC=E4=B8=80=E8=A6=A7=E3=80=81=E8=A9=B3=E7=B4=B0?= =?UTF-8?q?=E7=94=BB=E9=9D=A2=E3=81=AE=E3=83=95=E3=82=A9=E3=83=B3=E3=83=88?= =?UTF-8?q?=E3=82=B5=E3=82=A4=E3=82=BA=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/entry/entry-detail.tsx | 4 ++-- src/components/entry/entry-row.tsx | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/entry/entry-detail.tsx b/src/components/entry/entry-detail.tsx index 4d002ce..2853289 100644 --- a/src/components/entry/entry-detail.tsx +++ b/src/components/entry/entry-detail.tsx @@ -66,7 +66,7 @@ export function EntryDetailView({ entry }: Props) { {v.field.label} - {v.value ?? ""} + {v.value ?? ""} ))} diff --git a/src/components/entry/entry-row.tsx b/src/components/entry/entry-row.tsx index 34197fc..ab8d81f 100644 --- a/src/components/entry/entry-row.tsx +++ b/src/components/entry/entry-row.tsx @@ -24,15 +24,15 @@ export function EntryRow({ entry }: Props) { modifiers={[foregroundStyle({ type: "hierarchical", style: "primary" })]} onPress={() => router.push(`/(journal)/entry/${entry.id}`)} > - - + + {entry.title} {entry.preview} - + {formatDate(entry.date)} From 49f6f2536423c42e5e1cc656f78dbeac53d8df0d Mon Sep 17 00:00:00 2001 From: 273* Date: Sun, 10 May 2026 11:47:56 +0900 Subject: [PATCH 04/23] =?UTF-8?q?feat:=20=E3=82=A8=E3=83=B3=E3=83=88?= =?UTF-8?q?=E3=83=AA=E3=83=BC=E4=BD=9C=E6=88=90=E7=94=BB=E9=9D=A2=E3=82=92?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(journal)/entry/create.tsx | 126 +++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 src/app/(journal)/entry/create.tsx diff --git a/src/app/(journal)/entry/create.tsx b/src/app/(journal)/entry/create.tsx new file mode 100644 index 0000000..f26681d --- /dev/null +++ b/src/app/(journal)/entry/create.tsx @@ -0,0 +1,126 @@ +import { + PlatformColor, + Pressable, + Text as RNText, + StyleSheet, + View, +} from "react-native"; + +import { + Host, + List, + Section, + Text, + TextField, + VStack, +} from "@expo/ui/swift-ui"; +import { + font, + foregroundStyle, + frame, + listStyle, +} from "@expo/ui/swift-ui/modifiers"; +import { Stack, useLocalSearchParams, useRouter } from "expo-router"; +import { SymbolView } from "expo-symbols"; + +import { formatDate } from "@/utils/date"; +import { useJournalField } from "@/utils/journal/use-journal-field"; + +/** + * エントリー作成 + */ +export default function EntryCreateScreen() { + const router = useRouter(); + + const { name } = useLocalSearchParams<{ name: string }>(); + + const { createJournal, formDisabled } = useJournalField(); + + const handleCreate = async () => { + const { id } = await createJournal(); + + // replace でスタックせずにジャーナル詳細画面からジャーナル一覧へ戻れるようにする + router.replace(`/(journal)/entry/${id}}`); + }; + + const now = Date.now(); + + return ( + <> + ( + + New Entry + {name} + + ), + headerRight: () => ( + + + + ), + }} + /> + + + +
+ + + hogehoge + + + + +
+
+
+
+ + ); +} + +const styles = StyleSheet.create({ + headerTitle: { + alignItems: "center", + }, + title: { + fontSize: 17, + fontWeight: "600", + color: PlatformColor("label"), + }, + subtitle: { + fontSize: 12, + color: PlatformColor("secondaryLabel"), + }, +}); From 4633bccaa45a393e0f37b44b0d1cf2864911f458 Mon Sep 17 00:00:00 2001 From: 273* Date: Sun, 10 May 2026 12:08:37 +0900 Subject: [PATCH 05/23] =?UTF-8?q?refactor:=20=E3=82=B8=E3=83=A3=E3=83=BC?= =?UTF-8?q?=E3=83=8A=E3=83=AB=E3=81=AE=E5=9E=8B=E3=81=AE=E3=83=AA=E3=83=95?= =?UTF-8?q?=E3=82=A1=E3=82=AF=E3=82=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/journal/journal-create-view.tsx | 4 ++-- src/db/schemas/entries.ts | 1 + src/utils/journal/use-journal-field.ts | 15 ++++----------- 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/src/components/journal/journal-create-view.tsx b/src/components/journal/journal-create-view.tsx index 34b501b..956c254 100644 --- a/src/components/journal/journal-create-view.tsx +++ b/src/components/journal/journal-create-view.tsx @@ -25,7 +25,7 @@ import { import { FIELD_ICONS, FIELD_LABELS, FieldType } from "@/core/constants"; import { JournalMetaObj, - type FieldObj, + type FieldOmitSortObj, } from "@/utils/journal/use-journal-field"; import { FieldBottomSheet } from "./field-bottom-sheet"; @@ -46,7 +46,7 @@ function isValidColor(hex: string): boolean { type Props = { /** フィールド一覧 */ - fields: FieldObj[]; + fields: FieldOmitSortObj[]; /** フィールドを追加する関数 */ addField: (type: FieldType) => void; /** フィールドのラベルを更新する関数 */ diff --git a/src/db/schemas/entries.ts b/src/db/schemas/entries.ts index 198f776..aa8b8d0 100644 --- a/src/db/schemas/entries.ts +++ b/src/db/schemas/entries.ts @@ -53,6 +53,7 @@ export const entriesRelations = relations(entries, ({ one, many }) => ({ fields: [entries.journalId], references: [journals.id], }), + // entry.enry_value ではなく entry.valueで取ると命名が綺麗 values: many(entryValues), })); diff --git a/src/utils/journal/use-journal-field.ts b/src/utils/journal/use-journal-field.ts index 508a874..0835eb8 100644 --- a/src/utils/journal/use-journal-field.ts +++ b/src/utils/journal/use-journal-field.ts @@ -22,14 +22,7 @@ export type JournalMetaObj = { /** * ジャーナルフィールドの1項目 */ -export type FieldObj = { - /** フィールドID */ - id: string; - /** フィールド種別 */ - type: FieldType; - /** 表示ラベル */ - label: string; -}; +export type FieldOmitSortObj = Omit; /** * 全 FieldType の配列 @@ -37,7 +30,7 @@ export type FieldObj = { export const FIELD_TYPES = Object.keys(FIELD_ICONS) as FieldType[]; /** - * ジャーナルフィールドの管理フック + * ジャーナルフィールドに関するフック * @returns * - fields 現在のフィールド一覧 * - addField 新規フィールドを追加する関数 @@ -50,7 +43,7 @@ export const FIELD_TYPES = Object.keys(FIELD_ICONS) as FieldType[]; * - formDisabled フォームが送信可能かどうかのフラグ */ export const useJournalField = () => { - const [fields, setFields] = useState([]); + const [fields, setFields] = useState([]); const initialState = { name: "", @@ -65,7 +58,7 @@ export const useJournalField = () => { * @param type 追加するフィールドの種別 */ const addField = (type: FieldType): void => { - const newField: FieldObj = { + const newField: FieldOmitSortObj = { id: Crypto.randomUUID(), type, label: "", From d0b02d1bb3e137d7696be692697184a91039f3c1 Mon Sep 17 00:00:00 2001 From: 273* Date: Sun, 10 May 2026 12:59:12 +0900 Subject: [PATCH 06/23] =?UTF-8?q?feat:=20=E3=82=A8=E3=83=B3=E3=83=88?= =?UTF-8?q?=E3=83=AA=E3=83=BC=E4=BD=9C=E6=88=90=E7=94=BB=E9=9D=A2=E3=82=92?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(journal)/entry/create.tsx | 58 ++------------------ src/components/entry/entry-create-view.tsx | 62 ++++++++++++++++++++++ 2 files changed, 65 insertions(+), 55 deletions(-) create mode 100644 src/components/entry/entry-create-view.tsx diff --git a/src/app/(journal)/entry/create.tsx b/src/app/(journal)/entry/create.tsx index f26681d..1291fee 100644 --- a/src/app/(journal)/entry/create.tsx +++ b/src/app/(journal)/entry/create.tsx @@ -6,24 +6,10 @@ import { View, } from "react-native"; -import { - Host, - List, - Section, - Text, - TextField, - VStack, -} from "@expo/ui/swift-ui"; -import { - font, - foregroundStyle, - frame, - listStyle, -} from "@expo/ui/swift-ui/modifiers"; import { Stack, useLocalSearchParams, useRouter } from "expo-router"; import { SymbolView } from "expo-symbols"; -import { formatDate } from "@/utils/date"; +import { EntryCreateView } from "@/components/entry/entry-create-view"; import { useJournalField } from "@/utils/journal/use-journal-field"; /** @@ -43,8 +29,6 @@ export default function EntryCreateScreen() { router.replace(`/(journal)/entry/${id}}`); }; - const now = Date.now(); - return ( <> - - - -
- - - hogehoge - - - - -
-
-
-
+ ); } @@ -120,7 +68,7 @@ const styles = StyleSheet.create({ color: PlatformColor("label"), }, subtitle: { - fontSize: 12, + fontSize: 14, color: PlatformColor("secondaryLabel"), }, }); diff --git a/src/components/entry/entry-create-view.tsx b/src/components/entry/entry-create-view.tsx new file mode 100644 index 0000000..4c4f19f --- /dev/null +++ b/src/components/entry/entry-create-view.tsx @@ -0,0 +1,62 @@ +import { PlatformColor, View } from "react-native"; + +import { + Host, + List, + Section, + Text, + TextField, + VStack, +} from "@expo/ui/swift-ui"; +import { + font, + foregroundStyle, + frame, + listStyle, +} from "@expo/ui/swift-ui/modifiers"; + +import { formatDate } from "@/utils/date"; + +export function EntryCreateView() { + const now = Date.now(); + + return ( + + + +
+ + + hogehoge + + + + +
+
+
+
+ ); +} From 1547bea5cb62d6981d552323f057e148013757a4 Mon Sep 17 00:00:00 2001 From: 273* Date: Sun, 10 May 2026 14:12:35 +0900 Subject: [PATCH 07/23] =?UTF-8?q?fix:=20=E3=82=A8=E3=83=B3=E3=83=88?= =?UTF-8?q?=E3=83=AA=E3=83=BC=E8=A9=B3=E7=B4=B0=E3=81=AE=E3=83=95=E3=82=A9?= =?UTF-8?q?=E3=83=B3=E3=83=88=E3=82=B5=E3=82=A4=E3=82=BA=E3=82=92=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/entry/entry-detail.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/entry/entry-detail.tsx b/src/components/entry/entry-detail.tsx index 2853289..53801a9 100644 --- a/src/components/entry/entry-detail.tsx +++ b/src/components/entry/entry-detail.tsx @@ -24,6 +24,7 @@ type Props = { /** エントリーデータ */ entry: EntryDetailObj; }; + /** * エントリー詳細画面 */ @@ -75,7 +76,7 @@ export function EntryDetailView({ entry }: Props) { > {v.field.label}
- {v.value ?? ""} + {v.value ?? ""}
))} From 5f1e116b5cf9dcacfe552b6758c5fb97ef7806c6 Mon Sep 17 00:00:00 2001 From: 273* Date: Sun, 10 May 2026 15:17:31 +0900 Subject: [PATCH 08/23] =?UTF-8?q?feat:=20=E3=82=A8=E3=83=B3=E3=83=88?= =?UTF-8?q?=E3=83=AA=E3=83=BC=E3=81=AE=E3=83=95=E3=82=A3=E3=83=BC=E3=83=AB?= =?UTF-8?q?=E3=83=89=E3=82=B3=E3=83=B3=E3=83=9D=E3=83=BC=E3=83=8D=E3=83=B3?= =?UTF-8?q?=E3=83=88=E3=82=92=E4=BD=9C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/field/emtry-number.tsx | 66 +++++++++++++++++++++ src/components/field/entry-check.tsx | 75 ++++++++++++++++++++++++ src/components/field/entry-date.tsx | 58 ++++++++++++++++++ src/components/field/entry-location.tsx | 70 ++++++++++++++++++++++ src/components/field/entry-long-text.tsx | 60 +++++++++++++++++++ src/components/field/entry-media.tsx | 58 ++++++++++++++++++ src/components/field/entry-text.tsx | 51 ++++++++++++++++ src/components/field/entry-time.tsx | 58 ++++++++++++++++++ 8 files changed, 496 insertions(+) create mode 100644 src/components/field/emtry-number.tsx create mode 100644 src/components/field/entry-check.tsx create mode 100644 src/components/field/entry-date.tsx create mode 100644 src/components/field/entry-location.tsx create mode 100644 src/components/field/entry-long-text.tsx create mode 100644 src/components/field/entry-media.tsx create mode 100644 src/components/field/entry-text.tsx create mode 100644 src/components/field/entry-time.tsx diff --git a/src/components/field/emtry-number.tsx b/src/components/field/emtry-number.tsx new file mode 100644 index 0000000..e10c506 --- /dev/null +++ b/src/components/field/emtry-number.tsx @@ -0,0 +1,66 @@ +import { useRef, useState } from "react"; + +import { Text, TextField, type TextFieldRef, VStack } from "@expo/ui/swift-ui"; +import { font, foregroundStyle, frame } from "@expo/ui/swift-ui/modifiers"; + +import { FIELD_LABELS } from "@/core/constants"; + +type Props = { + /** フィールドラベル */ + label: string; + /** デフォルト値 */ + defaultValue?: number; + /** 値変更時のコールバック */ + onValueChange?: (value: number) => void | Promise; + /** 入力かどうか */ + edit?: boolean; +}; + +/** + * 数値フィールド + */ +export function EntryNumber({ + label, + defaultValue = 0, + onValueChange, + edit = false, +}: Props) { + const [number, setNumber] = useState(defaultValue); + const numberFieldRef = useRef(null); + + return ( + + + {label} + + {edit ? ( + { + const cleaned = v + .replace(/[^0-9.]/g, "") + .replace(/^(\d*\.?\d*).*/, "$1"); + if (cleaned !== v) numberFieldRef.current?.setText(cleaned); + const parsed = + cleaned === "" || cleaned === "." ? 0 : parseFloat(cleaned); + setNumber(parsed); + onValueChange?.(parsed); + }} + modifiers={[frame({ maxWidth: 9999 })]} + /> + ) : ( + {String(number)} + )} + + ); +} diff --git a/src/components/field/entry-check.tsx b/src/components/field/entry-check.tsx new file mode 100644 index 0000000..27c0081 --- /dev/null +++ b/src/components/field/entry-check.tsx @@ -0,0 +1,75 @@ +import { useState } from "react"; +import { PlatformColor } from "react-native"; + +import { Button, HStack, Image, Text } from "@expo/ui/swift-ui"; +import { + buttonStyle, + contentShape, + foregroundStyle, + frame, + shapes, +} from "@expo/ui/swift-ui/modifiers"; + +type Props = { + /** フィールドラベル */ + label: string; + /** デフォルト値 */ + defaultValue?: boolean; + /** 値変更時のコールバック */ + onValueChange?: (value: boolean) => void | Promise; + /** 入力かどうか */ + edit?: boolean; +}; + +/** + * チェックフィールド + */ +export function EntryCheck({ + label, + defaultValue = false, + onValueChange, + edit = false, +}: Props) { + const [check, setCheck] = useState(defaultValue); + + const handlePress = () => { + if (!edit) return; + const next = !check; + setCheck(next); + onValueChange?.(next); + }; + + return ( + + ); +} diff --git a/src/components/field/entry-date.tsx b/src/components/field/entry-date.tsx new file mode 100644 index 0000000..a5a70a3 --- /dev/null +++ b/src/components/field/entry-date.tsx @@ -0,0 +1,58 @@ +import { useState } from "react"; + +import { DatePicker, Text, VStack } from "@expo/ui/swift-ui"; +import { font, foregroundStyle } from "@expo/ui/swift-ui/modifiers"; + +import { FIELD_LABELS } from "@/core/constants"; +import { formatDate } from "@/utils/date"; + +type Props = { + /** フィールドラベル */ + label: string; + /** デフォルト値 */ + defaultValue?: Date; + /** 値変更時のコールバック */ + onValueChange?: (value: Date) => void | Promise; + /** 入力かどうか */ + edit?: boolean; +}; + +/** + * 日付フィールド + */ +export function EntryDate({ + label, + defaultValue = new Date(), + onValueChange, + edit = false, +}: Props) { + const [date, setDate] = useState(defaultValue); + + const handleChange = (value: Date) => { + setDate(value); + onValueChange?.(value); + }; + + return ( + + + {label} + + {edit ? ( + + ) : ( + {formatDate(date)} + )} + + ); +} diff --git a/src/components/field/entry-location.tsx b/src/components/field/entry-location.tsx new file mode 100644 index 0000000..308b04b --- /dev/null +++ b/src/components/field/entry-location.tsx @@ -0,0 +1,70 @@ +import { PlatformColor } from "react-native"; + +import { Button, HStack, Image, Spacer, Text, VStack } from "@expo/ui/swift-ui"; +import { + buttonStyle, + font, + foregroundStyle, +} from "@expo/ui/swift-ui/modifiers"; + +type Props = { + /** フィールドラベル */ + label: string; + /** 入力かどうか */ + edit?: boolean; +}; + +/** + * 位置情報フィールド(ダミー実装) + */ +export function EntryLocation({ label, edit = false }: Props) { + return ( + + + {label} + + {edit ? ( + + Media + + + + ) : ( + + + + 未設定 + + + )} + + ); +} diff --git a/src/components/field/entry-long-text.tsx b/src/components/field/entry-long-text.tsx new file mode 100644 index 0000000..0b77094 --- /dev/null +++ b/src/components/field/entry-long-text.tsx @@ -0,0 +1,60 @@ +import { Text, TextField, VStack } from "@expo/ui/swift-ui"; +import { + font, + foregroundStyle, + frame, + lineLimit, +} from "@expo/ui/swift-ui/modifiers"; + +import { FIELD_LABELS } from "@/core/constants"; + +type Props = { + /** フィールドラベル */ + label: string; + /** デフォルト値 */ + defaultValue?: string; + /** 値変更時のコールバック */ + onValueChange?: (value: string) => void | Promise; + /** 入力かどうか */ + edit?: boolean; +}; + +/** + * ロングテキストフィールド + */ +export function EntryLongText({ + label, + defaultValue = "", + onValueChange, + edit = false, +}: Props) { + return ( + + + {label} + + {edit ? ( + + ) : ( + {defaultValue} + )} + + ); +} diff --git a/src/components/field/entry-media.tsx b/src/components/field/entry-media.tsx new file mode 100644 index 0000000..e15f41b --- /dev/null +++ b/src/components/field/entry-media.tsx @@ -0,0 +1,58 @@ +import { PlatformColor } from "react-native"; + +import { Button, HStack, Image, Spacer, Text, VStack } from "@expo/ui/swift-ui"; +import { + buttonStyle, + font, + foregroundStyle, +} from "@expo/ui/swift-ui/modifiers"; + +type Props = { + /** フィールドラベル */ + label: string; + /** 入力かどうか */ + edit?: boolean; +}; + +/** + * メディアフィールド(ダミー実装) + */ +export function EntryMedia({ label, edit = false }: Props) { + return ( + + + {label} + + {edit ? ( + + Media + + + + ) : ( + + )} + + ); +} diff --git a/src/components/field/entry-text.tsx b/src/components/field/entry-text.tsx new file mode 100644 index 0000000..1c2659d --- /dev/null +++ b/src/components/field/entry-text.tsx @@ -0,0 +1,51 @@ +import { Text, TextField, VStack } from "@expo/ui/swift-ui"; +import { font, foregroundStyle, frame } from "@expo/ui/swift-ui/modifiers"; + +import { FIELD_LABELS } from "@/core/constants"; + +type Props = { + /** フィールドラベル */ + label: string; + /** デフォルト値 */ + defaultValue?: string; + /** 値変更時のコールバック */ + onValueChange?: (value: string) => void | Promise; + /** 入力かどうか */ + edit?: boolean; +}; + +/** + * テキストフィールド + */ +export function EntryText({ + label, + defaultValue = "", + onValueChange, + edit = false, +}: Props) { + return ( + + + {label} + + {edit ? ( + + ) : ( + {defaultValue} + )} + + ); +} diff --git a/src/components/field/entry-time.tsx b/src/components/field/entry-time.tsx new file mode 100644 index 0000000..3b612aa --- /dev/null +++ b/src/components/field/entry-time.tsx @@ -0,0 +1,58 @@ +import { useState } from "react"; + +import { DatePicker, Text, VStack } from "@expo/ui/swift-ui"; +import { font, foregroundStyle } from "@expo/ui/swift-ui/modifiers"; + +import { FIELD_LABELS } from "@/core/constants"; +import { formatTime } from "@/utils/date"; + +type Props = { + /** フィールドラベル */ + label: string; + /** デフォルト値 */ + defaultValue?: Date; + /** 値変更時のコールバック */ + onValueChange?: (value: Date) => void | Promise; + /** 入力かどうか */ + edit?: boolean; +}; + +/** + * 時刻フィールド + */ +export function EntryTime({ + label, + defaultValue = new Date(), + onValueChange, + edit = false, +}: Props) { + const [time, setTime] = useState(defaultValue); + + const handleChange = (value: Date) => { + setTime(value); + onValueChange?.(value); + }; + + return ( + + + {label} + + {edit ? ( + + ) : ( + {formatTime(time)} + )} + + ); +} From 90248e7d84c1c61288a23b2115f64a68f9ac836b Mon Sep 17 00:00:00 2001 From: 273* Date: Sun, 10 May 2026 15:20:05 +0900 Subject: [PATCH 09/23] =?UTF-8?q?feat:=20=E3=83=80=E3=83=9F=E3=83=BC?= =?UTF-8?q?=E3=81=AE=E3=82=A8=E3=83=B3=E3=83=88=E3=83=AA=E3=83=BC=E3=83=95?= =?UTF-8?q?=E3=82=A3=E3=83=BC=E3=83=AB=E3=83=89=E3=82=92=E4=BD=9C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/explore.tsx | 6 ++- src/components/all-form-list.tsx | 45 ++++++++++++++++++++++ src/components/entry/entry-create-view.tsx | 3 ++ 3 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 src/components/all-form-list.tsx diff --git a/src/app/explore.tsx b/src/app/explore.tsx index 9cae1f5..6cf1c52 100644 --- a/src/app/explore.tsx +++ b/src/app/explore.tsx @@ -1,6 +1,8 @@ import { PlatformColor, View } from "react-native"; -import { Host, Text } from "@expo/ui/swift-ui"; +import { Host } from "@expo/ui/swift-ui"; + +import { AllformList } from "@/components/all-form-list"; export default function ExploreScreen() { return ( @@ -9,7 +11,7 @@ export default function ExploreScreen() { style={{ flex: 1, backgroundColor: PlatformColor("systemBackground") }} useViewportSizeMeasurement > - Explore + ); diff --git a/src/components/all-form-list.tsx b/src/components/all-form-list.tsx new file mode 100644 index 0000000..5b94c78 --- /dev/null +++ b/src/components/all-form-list.tsx @@ -0,0 +1,45 @@ +import { List, Section } from "@expo/ui/swift-ui"; +import { frame, listStyle } from "@expo/ui/swift-ui/modifiers"; + +import { EntryNumber } from "./field/emtry-number"; +import { EntryCheck } from "./field/entry-check"; +import { EntryDate } from "./field/entry-date"; +import { EntryLocation } from "./field/entry-location"; +import { EntryLongText } from "./field/entry-long-text"; +import { EntryMedia } from "./field/entry-media"; +import { EntryText } from "./field/entry-text"; +import { EntryTime } from "./field/entry-time"; + +const edit = false; +/** + * 全フィールドタイプのフォームサンプル + */ +export function AllformList() { + return ( + +
+ + + + + + + + +
+
+ ); +} diff --git a/src/components/entry/entry-create-view.tsx b/src/components/entry/entry-create-view.tsx index 4c4f19f..be5d0a7 100644 --- a/src/components/entry/entry-create-view.tsx +++ b/src/components/entry/entry-create-view.tsx @@ -17,6 +17,9 @@ import { import { formatDate } from "@/utils/date"; +/** + * エントリー作成画面 + */ export function EntryCreateView() { const now = Date.now(); From e4b47c3e75ce306c3e49e90230c1e99bd3e66325 Mon Sep 17 00:00:00 2001 From: 273* Date: Sun, 10 May 2026 15:20:37 +0900 Subject: [PATCH 10/23] =?UTF-8?q?feat:=20=E6=99=82=E9=96=93=E3=81=AE?= =?UTF-8?q?=E3=83=95=E3=82=A9=E3=83=BC=E3=83=9E=E3=83=83=E3=83=88=E9=96=A2?= =?UTF-8?q?=E6=95=B0=E3=82=92=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/date.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/utils/date.ts b/src/utils/date.ts index c23ff59..217efe6 100644 --- a/src/utils/date.ts +++ b/src/utils/date.ts @@ -12,6 +12,18 @@ export const formatDate = (date: Date | number): string => { }).format(date); }; +/** + * 時刻を "13:05" などロケールに応じた形式でフォーマットする関数 + * @param date フォーマットする日付 + * @returns フォーマットされた時刻文字列 + */ +export const formatTime = (date: Date | number): string => { + return new Intl.DateTimeFormat(undefined, { + hour: "numeric", + minute: "2-digit", + }).format(date); +}; + /** * 年月を "2026年5月" / "May 2026" などロケールに応じた形式でフォーマットする関数 * @param date フォーマットする日付 From e45f60696f781a0e367c233b9d19cbfb08895b37 Mon Sep 17 00:00:00 2001 From: 273* Date: Sun, 10 May 2026 16:34:07 +0900 Subject: [PATCH 11/23] =?UTF-8?q?refactor:=20fieldobj=E3=81=AE=E5=9E=8B?= =?UTF-8?q?=E5=90=8D=E3=82=92=E9=81=A9=E5=88=87=E3=81=AA=E3=82=82=E3=81=AE?= =?UTF-8?q?=E3=81=AB=E3=83=AA=E3=83=95=E3=82=A1=E3=82=AF=E3=82=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(journal)/create.tsx | 4 ++-- src/components/journal/journal-create-view.tsx | 4 ++-- src/db/queries/journals.ts | 5 +++-- src/db/schemas/fields.ts | 2 +- src/utils/journal/use-journal-field.ts | 15 ++++++++++----- 5 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/app/(journal)/create.tsx b/src/app/(journal)/create.tsx index 23c0e8a..c7c1b92 100644 --- a/src/app/(journal)/create.tsx +++ b/src/app/(journal)/create.tsx @@ -24,7 +24,7 @@ export default function JournalCreateScreen() { formDisabled, } = useJournalField(); - const handleCreate = async () => { + const handleJournalCreate = async () => { const { id, name } = await createJournal(); // replace でスタックせずにジャーナル詳細画面からジャーナル一覧へ戻れるようにする @@ -37,7 +37,7 @@ export default function JournalCreateScreen() { options={{ title: "New Journal", headerRight: () => ( - + void; /** フィールドのラベルを更新する関数 */ diff --git a/src/db/queries/journals.ts b/src/db/queries/journals.ts index 733bdf3..52193c6 100644 --- a/src/db/queries/journals.ts +++ b/src/db/queries/journals.ts @@ -1,7 +1,8 @@ import { sql } from "drizzle-orm"; import { db } from "@/db/client"; -import { entries, FieldlObj, fields, JournalObj, journals } from "@/db/schemas"; +import { entries, fields, JournalObj, journals } from "@/db/schemas"; +import { FieldWithSortObj } from "@/utils/journal/use-journal-field"; /** * ジャーナル一覧を取得するクエリ @@ -22,7 +23,7 @@ export const getJournalsQuery = db.query.journals.findMany({ */ export const storeJournal = async ( journal: JournalObj, - newFields: FieldlObj[], + newFields: FieldWithSortObj[], ) => { await db.transaction(async (tx) => { await tx.insert(journals).values(journal); diff --git a/src/db/schemas/fields.ts b/src/db/schemas/fields.ts index 77a5466..e4c2229 100644 --- a/src/db/schemas/fields.ts +++ b/src/db/schemas/fields.ts @@ -29,4 +29,4 @@ export const fieldsRelations = relations(fields, ({ one, many }) => ({ entryValues: many(entryValues), })); -export type FieldlObj = Omit; +export type FieldlObj = typeof fields.$inferInsert; diff --git a/src/utils/journal/use-journal-field.ts b/src/utils/journal/use-journal-field.ts index 0835eb8..4faa1a1 100644 --- a/src/utils/journal/use-journal-field.ts +++ b/src/utils/journal/use-journal-field.ts @@ -20,9 +20,14 @@ export type JournalMetaObj = { }; /** - * ジャーナルフィールドの1項目 + * ジャーナル作成フォームのフィールド下書き(journalId・sortOrder なし) */ -export type FieldOmitSortObj = Omit; +export type FieldDraftObj = Omit; + +/** + * sortOrder 確定済み・journalId 未割当のフィールド(DB 保存直前) + */ +export type FieldWithSortObj = Omit; /** * 全 FieldType の配列 @@ -43,7 +48,7 @@ export const FIELD_TYPES = Object.keys(FIELD_ICONS) as FieldType[]; * - formDisabled フォームが送信可能かどうかのフラグ */ export const useJournalField = () => { - const [fields, setFields] = useState([]); + const [fields, setFields] = useState([]); const initialState = { name: "", @@ -58,7 +63,7 @@ export const useJournalField = () => { * @param type 追加するフィールドの種別 */ const addField = (type: FieldType): void => { - const newField: FieldOmitSortObj = { + const newField: FieldDraftObj = { id: Crypto.randomUUID(), type, label: "", @@ -125,7 +130,7 @@ export const useJournalField = () => { updatedAt: now, }; - const newFieldsFieldlObj: FieldlObj[] = fields.map((field, i) => ({ + const newFieldsFieldlObj: FieldWithSortObj[] = fields.map((field, i) => ({ id: field.id, type: field.type, label: field.label, From c7fe5462fb34e1be40d0fb7837c1507c7f613d7d Mon Sep 17 00:00:00 2001 From: 273* Date: Sun, 10 May 2026 16:44:38 +0900 Subject: [PATCH 12/23] =?UTF-8?q?feat:=20=E3=83=A2=E3=83=83=E3=82=AF?= =?UTF-8?q?=E3=82=92=E5=89=8A=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/mocks/entries.ts | 79 -------------------------------------------- 1 file changed, 79 deletions(-) delete mode 100644 src/mocks/entries.ts diff --git a/src/mocks/entries.ts b/src/mocks/entries.ts deleted file mode 100644 index 449e207..0000000 --- a/src/mocks/entries.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { EntryObj } from "@/utils/journal/use-entry"; - -export const ENTRIES: EntryObj[] = [ - { - id: "1", - date: new Date(2026, 4, 5), - title: "朝の振り返り", - preview: - "今日は早起きできた。天気がよくて気分も上々。散歩しながらいろいろ考えた。やっぱり朝の時間は大切だと感じる。", - bookmark: true, - }, - { - id: "2", - date: new Date(2026, 4, 4), - title: "作業記録", - preview: - "新機能の実装を進めた。バグをいくつか修正して、全体的にスムーズに動くようになってきた。", - }, - { - id: "3", - date: new Date(2026, 4, 3), - title: "週末の過ごし方", - preview: - "久しぶりに友人と会った。話が弾んで気づいたら夕方になっていた。やっぱり人と話すのは大切。", - }, - { - id: "4", - date: new Date(2026, 4, 2), - title: "読書メモ", - preview: - "「習慣の力」を読んだ。習慣ループの話が面白かった。自分の習慣を見直すきっかけになりそう。", - bookmark: true, - }, - { - id: "5", - date: new Date(2026, 4, 1), - title: "月初めの目標", - preview: - "5月の目標を書き出した。毎朝30分読書、週3回運動、日記を毎日書くこと。無理のない範囲で続けたい。", - bookmark: true, - }, - { - id: "6", - date: new Date(2026, 3, 30), - title: "夜の散歩", - preview: - "仕事終わりに少し歩いた。夜風が気持ちよくて、頭が整理された気がする。こういう時間が必要だと思う。", - }, - { - id: "7", - date: new Date(2026, 3, 28), - title: "新しいレシピ", - preview: - "チキンカレーを初めて自分で作った。思ったより簡単で、味もよかった。また作ってみたい。", - bookmark: true, - }, - { - id: "8", - date: new Date(2026, 3, 26), - title: "気になったこと", - preview: - "最近、集中力が続かないと感じる。スマホの使い過ぎかもしれない。意識して使用時間を減らしてみよう。", - }, - { - id: "9", - date: new Date(2026, 3, 24), - title: "映画の感想", - preview: - "「PERFECT DAYS」を観た。台詞が少ないのに、主人公の日常がとても豊かに見えた。自分も丁寧に生きたいと思った。", - bookmark: true, - }, - { - id: "10", - date: new Date(2026, 3, 22), - title: "運動の記録", - preview: - "30分ジョギングした。最初はきつかったが、後半は気持ちよくなってきた。継続することが大事だと実感。", - }, -]; From 14fcfe6f479396b732447a30d807a45f5ef59aaa Mon Sep 17 00:00:00 2001 From: 273* Date: Sun, 10 May 2026 17:59:31 +0900 Subject: [PATCH 13/23] =?UTF-8?q?feat:=20=E3=82=A8=E3=83=B3=E3=83=88?= =?UTF-8?q?=E3=83=AA=E3=83=BC=E3=82=92=E4=BD=9C=E6=88=90=E3=81=99=E3=82=8B?= =?UTF-8?q?=E7=94=BB=E9=9D=A2=E3=82=92=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(journal)/entry/create.tsx | 24 ++++--- src/components/entry/entry-create-view.tsx | 60 +++++++---------- src/components/entry/entry-list-view.tsx | 2 +- src/components/entry/entry-render-field.tsx | 71 +++++++++++++++++++++ src/components/field/emtry-number.tsx | 4 +- src/db/queries/fields.ts | 10 +++ src/utils/journal/use-entry.ts | 47 ++++++++++++++ 7 files changed, 165 insertions(+), 53 deletions(-) create mode 100644 src/components/entry/entry-render-field.tsx create mode 100644 src/db/queries/fields.ts diff --git a/src/app/(journal)/entry/create.tsx b/src/app/(journal)/entry/create.tsx index 1291fee..090dda1 100644 --- a/src/app/(journal)/entry/create.tsx +++ b/src/app/(journal)/entry/create.tsx @@ -6,27 +6,25 @@ import { View, } from "react-native"; -import { Stack, useLocalSearchParams, useRouter } from "expo-router"; +import { Stack, useLocalSearchParams } from "expo-router"; import { SymbolView } from "expo-symbols"; import { EntryCreateView } from "@/components/entry/entry-create-view"; -import { useJournalField } from "@/utils/journal/use-journal-field"; + +const formDisabled = false; /** * エントリー作成 */ export default function EntryCreateScreen() { - const router = useRouter(); - - const { name } = useLocalSearchParams<{ name: string }>(); - - const { createJournal, formDisabled } = useJournalField(); + // const router = useRouter(); - const handleCreate = async () => { - const { id } = await createJournal(); + const { id, name } = useLocalSearchParams<{ id: string; name: string }>(); - // replace でスタックせずにジャーナル詳細画面からジャーナル一覧へ戻れるようにする - router.replace(`/(journal)/entry/${id}}`); + const handleEntryCreate = async () => { + // const { id } = await createJournal(); + // // replace でスタックせずにジャーナル詳細画面からジャーナル一覧へ戻れるようにする + // router.replace(`/(journal)/entry/${id}}`); }; return ( @@ -40,7 +38,7 @@ export default function EntryCreateScreen() { ), headerRight: () => ( - + - + ); } diff --git a/src/components/entry/entry-create-view.tsx b/src/components/entry/entry-create-view.tsx index be5d0a7..39e2b70 100644 --- a/src/components/entry/entry-create-view.tsx +++ b/src/components/entry/entry-create-view.tsx @@ -1,35 +1,36 @@ import { PlatformColor, View } from "react-native"; -import { - Host, - List, - Section, - Text, - TextField, - VStack, -} from "@expo/ui/swift-ui"; -import { - font, - foregroundStyle, - frame, - listStyle, -} from "@expo/ui/swift-ui/modifiers"; +import { Host, List, Section } from "@expo/ui/swift-ui"; +import { frame, listStyle } from "@expo/ui/swift-ui/modifiers"; +import { useLiveQuery } from "drizzle-orm/expo-sqlite"; +import { getFieldsQuery } from "@/db/queries/fields"; import { formatDate } from "@/utils/date"; +import { useEntry } from "@/utils/journal/use-entry"; + +import { renderField } from "./entry-render-field"; + +type Props = { + id: string; + /** ジャーナル id */ +}; /** * エントリー作成画面 */ -export function EntryCreateView() { +export function EntryCreateView({ id }: Props) { const now = Date.now(); + console.log(id); + + const { data: fields } = useLiveQuery(getFieldsQuery(id)); + + const { values, setValue } = useEntry(fields); + if (!fields) return null; return (
- - - hogehoge - - - - + {fields.map((field) => + renderField(field, values[field.id], setValue), + )}
diff --git a/src/components/entry/entry-list-view.tsx b/src/components/entry/entry-list-view.tsx index 8f2c58c..0e716f0 100644 --- a/src/components/entry/entry-list-view.tsx +++ b/src/components/entry/entry-list-view.tsx @@ -118,7 +118,7 @@ export function EntryListView({ - router.push(`/(journal)/entry/create?name=${journalName}`) + router.push(`/(journal)/entry/create?id=${id}&name=${journalName}`) } style={styles.fab} > diff --git a/src/components/entry/entry-render-field.tsx b/src/components/entry/entry-render-field.tsx new file mode 100644 index 0000000..2e7f9a9 --- /dev/null +++ b/src/components/entry/entry-render-field.tsx @@ -0,0 +1,71 @@ +import { EntryNumber } from "@/components/field/emtry-number"; +import { EntryCheck } from "@/components/field/entry-check"; +import { EntryDate } from "@/components/field/entry-date"; +import { EntryLocation } from "@/components/field/entry-location"; +import { EntryLongText } from "@/components/field/entry-long-text"; +import { EntryMedia } from "@/components/field/entry-media"; +import { EntryText } from "@/components/field/entry-text"; +import { EntryTime } from "@/components/field/entry-time"; +import type { FieldlObj } from "@/db/schemas"; +import { type FieldValue } from "@/utils/journal/use-entry"; + +/** + * フィールドタイプによってコンポーネントを返す関数 + * @param field フィールド + * @param value 現在のフィールドの値 + * @param setValue フィールドに値を格納する関数 + */ +export const renderField = ( + field: FieldlObj, + value: FieldValue, + setValue: (id: string, value: FieldValue) => void, +): React.JSX.Element => { + const shared = { + label: field.label, + onValueChange: (v: T) => setValue(field.id, v), + edit: true, + }; + + switch (field.type) { + case "text": + return ( + + ); + case "longText": + return ( + + ); + case "number": + return ( + + ); + case "check": + return ( + + ); + case "date": + return ( + + ); + case "time": + return ( + + ); + case "media": + return ; + case "location": + return ; + } +}; diff --git a/src/components/field/emtry-number.tsx b/src/components/field/emtry-number.tsx index e10c506..d0988cc 100644 --- a/src/components/field/emtry-number.tsx +++ b/src/components/field/emtry-number.tsx @@ -21,11 +21,11 @@ type Props = { */ export function EntryNumber({ label, - defaultValue = 0, + defaultValue, onValueChange, edit = false, }: Props) { - const [number, setNumber] = useState(defaultValue); + const [number, setNumber] = useState(defaultValue); const numberFieldRef = useRef(null); return ( diff --git a/src/db/queries/fields.ts b/src/db/queries/fields.ts new file mode 100644 index 0000000..af5e3ec --- /dev/null +++ b/src/db/queries/fields.ts @@ -0,0 +1,10 @@ +import { db } from "../client"; + +/** + * ジャーナルに紐付いたフィールド一覧を取得するクエリ + * @param journalId ジャーナルID + */ +export const getFieldsQuery = (journalId: string) => + db.query.fields.findMany({ + where: (fields, { eq }) => eq(fields.journalId, journalId), + }); diff --git a/src/utils/journal/use-entry.ts b/src/utils/journal/use-entry.ts index d6bd3dc..d7c96c8 100644 --- a/src/utils/journal/use-entry.ts +++ b/src/utils/journal/use-entry.ts @@ -1,3 +1,8 @@ +import { useState } from "react"; + +import type { FieldType } from "@/core/constants"; +import { FieldlObj } from "@/db/schemas"; + export type EntryObj = { id: string; date: Date; @@ -5,3 +10,45 @@ export type EntryObj = { preview: string; bookmark?: boolean; }; + +export type FieldValue = string | number | boolean | Date | null; + +/** + * 各エントリー入力のデフォルト値 + * @param type フィールドタイプ + */ +const getDefaultValue = (type: FieldType): FieldValue => { + switch (type) { + case "text": + case "longText": + return ""; + case "number": + return null; + case "check": + return false; + case "date": + case "time": + return new Date(); + case "media": + case "location": + return null; + } +}; + +/** + * エントリーフォームの値を管理するフック + * @param fields ジャーナルに紐づくフィールド一覧 + * @returns + * - values 現在のフィールドの値 + * - setValue フィールドに値を格納する関数 + */ +export const useEntry = (fields: FieldlObj[]) => { + const [values, setValues] = useState>(() => + Object.fromEntries(fields.map((f) => [f.id, getDefaultValue(f.type)])), + ); + + const setValue = (id: string, value: FieldValue) => + setValues((prev) => ({ ...prev, [id]: value })); + + return { values, setValue }; +}; From 9cf09edd3049c86b5534320275be0609e8f01ece Mon Sep 17 00:00:00 2001 From: 273* Date: Sun, 10 May 2026 18:18:44 +0900 Subject: [PATCH 14/23] =?UTF-8?q?refactor:=20=E3=83=95=E3=82=A3=E3=83=BC?= =?UTF-8?q?=E3=83=AB=E3=83=89=E3=81=8C=E5=86=8D=E3=83=AC=E3=83=B3=E3=83=80?= =?UTF-8?q?=E3=83=AA=E3=83=B3=E3=82=B0=E3=81=95=E3=82=8C=E3=81=AA=E3=81=84?= =?UTF-8?q?=E3=82=88=E3=81=86=E3=83=AA=E3=83=95=E3=82=A1=E3=82=AF=E3=82=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/entry/entry-create-view.tsx | 13 ++-- src/components/entry/entry-field-item.tsx | 51 +++++++++++++++ src/components/entry/entry-render-field.tsx | 71 --------------------- 3 files changed, 60 insertions(+), 75 deletions(-) create mode 100644 src/components/entry/entry-field-item.tsx delete mode 100644 src/components/entry/entry-render-field.tsx diff --git a/src/components/entry/entry-create-view.tsx b/src/components/entry/entry-create-view.tsx index 39e2b70..42c1db7 100644 --- a/src/components/entry/entry-create-view.tsx +++ b/src/components/entry/entry-create-view.tsx @@ -8,7 +8,7 @@ import { getFieldsQuery } from "@/db/queries/fields"; import { formatDate } from "@/utils/date"; import { useEntry } from "@/utils/journal/use-entry"; -import { renderField } from "./entry-render-field"; +import { EntryFieldItem } from "./entry-field-item"; type Props = { id: string; @@ -40,9 +40,14 @@ export function EntryCreateView({ id }: Props) { ]} >
- {fields.map((field) => - renderField(field, values[field.id], setValue), - )} + {fields.map((field) => ( + + ))}
diff --git a/src/components/entry/entry-field-item.tsx b/src/components/entry/entry-field-item.tsx new file mode 100644 index 0000000..e270ae0 --- /dev/null +++ b/src/components/entry/entry-field-item.tsx @@ -0,0 +1,51 @@ +import { EntryNumber } from "@/components/field/emtry-number"; +import { EntryCheck } from "@/components/field/entry-check"; +import { EntryDate } from "@/components/field/entry-date"; +import { EntryLocation } from "@/components/field/entry-location"; +import { EntryLongText } from "@/components/field/entry-long-text"; +import { EntryMedia } from "@/components/field/entry-media"; +import { EntryText } from "@/components/field/entry-text"; +import { EntryTime } from "@/components/field/entry-time"; +import { FieldType } from "@/core/constants"; +import type { FieldlObj } from "@/db/schemas"; +import { type FieldValue } from "@/utils/journal/use-entry"; + +type Props = { + /** フィールド定義 */ + field: FieldlObj; + /** 現在の値 */ + value: FieldValue; + /** 値変更時のコールバック */ + setValue: (id: string, value: FieldValue) => void; +}; + +/** + * フィールドタイプによってコンポーネントを切り替えるコンポーネント + * React Compiler がこの単位で再レンダリングを最適化する + */ +export function EntryFieldItem({ field, value, setValue }: Props) { + const shared = { + label: field.label, + onValueChange: (v: T) => setValue(field.id, v), + edit: true, + }; + + switch (field.type as FieldType) { + case "text": + return ; + case "longText": + return ; + case "number": + return ; + case "check": + return ; + case "date": + return ; + case "time": + return ; + case "media": + return ; + case "location": + return ; + } +} diff --git a/src/components/entry/entry-render-field.tsx b/src/components/entry/entry-render-field.tsx deleted file mode 100644 index 2e7f9a9..0000000 --- a/src/components/entry/entry-render-field.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { EntryNumber } from "@/components/field/emtry-number"; -import { EntryCheck } from "@/components/field/entry-check"; -import { EntryDate } from "@/components/field/entry-date"; -import { EntryLocation } from "@/components/field/entry-location"; -import { EntryLongText } from "@/components/field/entry-long-text"; -import { EntryMedia } from "@/components/field/entry-media"; -import { EntryText } from "@/components/field/entry-text"; -import { EntryTime } from "@/components/field/entry-time"; -import type { FieldlObj } from "@/db/schemas"; -import { type FieldValue } from "@/utils/journal/use-entry"; - -/** - * フィールドタイプによってコンポーネントを返す関数 - * @param field フィールド - * @param value 現在のフィールドの値 - * @param setValue フィールドに値を格納する関数 - */ -export const renderField = ( - field: FieldlObj, - value: FieldValue, - setValue: (id: string, value: FieldValue) => void, -): React.JSX.Element => { - const shared = { - label: field.label, - onValueChange: (v: T) => setValue(field.id, v), - edit: true, - }; - - switch (field.type) { - case "text": - return ( - - ); - case "longText": - return ( - - ); - case "number": - return ( - - ); - case "check": - return ( - - ); - case "date": - return ( - - ); - case "time": - return ( - - ); - case "media": - return ; - case "location": - return ; - } -}; From 84aa68e8c02935f1dc8a1c33eb9d61ccef854219 Mon Sep 17 00:00:00 2001 From: 273* Date: Sun, 10 May 2026 18:26:37 +0900 Subject: [PATCH 15/23] =?UTF-8?q?refactor:=20entry=E3=82=AB=E3=82=B9?= =?UTF-8?q?=E3=82=BF=E3=83=A0=E3=83=95=E3=83=83=E3=82=AF=E3=81=AE=E3=83=87?= =?UTF-8?q?=E3=82=A3=E3=83=AC=E3=82=AF=E3=83=88=E3=83=AA=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/entry/entry-create-view.tsx | 2 +- src/components/entry/entry-field-item.tsx | 2 +- src/components/entry/entry-list-view.tsx | 2 +- src/components/entry/entry-row.tsx | 2 +- src/utils/{journal => entry}/use-entry.ts | 0 5 files changed, 4 insertions(+), 4 deletions(-) rename src/utils/{journal => entry}/use-entry.ts (100%) diff --git a/src/components/entry/entry-create-view.tsx b/src/components/entry/entry-create-view.tsx index 42c1db7..8f1c089 100644 --- a/src/components/entry/entry-create-view.tsx +++ b/src/components/entry/entry-create-view.tsx @@ -6,7 +6,7 @@ import { useLiveQuery } from "drizzle-orm/expo-sqlite"; import { getFieldsQuery } from "@/db/queries/fields"; import { formatDate } from "@/utils/date"; -import { useEntry } from "@/utils/journal/use-entry"; +import { useEntry } from "@/utils/entry/use-entry"; import { EntryFieldItem } from "./entry-field-item"; diff --git a/src/components/entry/entry-field-item.tsx b/src/components/entry/entry-field-item.tsx index e270ae0..82f4670 100644 --- a/src/components/entry/entry-field-item.tsx +++ b/src/components/entry/entry-field-item.tsx @@ -8,7 +8,7 @@ import { EntryText } from "@/components/field/entry-text"; import { EntryTime } from "@/components/field/entry-time"; import { FieldType } from "@/core/constants"; import type { FieldlObj } from "@/db/schemas"; -import { type FieldValue } from "@/utils/journal/use-entry"; +import { type FieldValue } from "@/utils/entry/use-entry"; type Props = { /** フィールド定義 */ diff --git a/src/components/entry/entry-list-view.tsx b/src/components/entry/entry-list-view.tsx index 0e716f0..3be2a00 100644 --- a/src/components/entry/entry-list-view.tsx +++ b/src/components/entry/entry-list-view.tsx @@ -14,7 +14,7 @@ import { SymbolView } from "expo-symbols"; import type { SortKey } from "@/app/(journal)/[id]"; import { getEntriesQuery } from "@/db/queries/entries"; import { formatYearMonth } from "@/utils/date"; -import { EntryObj } from "@/utils/journal/use-entry"; +import { EntryObj } from "@/utils/entry/use-entry"; import { EntryRow } from "./entry-row"; diff --git a/src/components/entry/entry-row.tsx b/src/components/entry/entry-row.tsx index ab8d81f..ab67560 100644 --- a/src/components/entry/entry-row.tsx +++ b/src/components/entry/entry-row.tsx @@ -5,7 +5,7 @@ import { font, foregroundStyle, lineLimit } from "@expo/ui/swift-ui/modifiers"; import { useRouter } from "expo-router"; import { formatDate } from "@/utils/date"; -import { EntryObj } from "@/utils/journal/use-entry"; +import { EntryObj } from "@/utils/entry/use-entry"; const secondary = foregroundStyle({ type: "hierarchical", style: "secondary" }); diff --git a/src/utils/journal/use-entry.ts b/src/utils/entry/use-entry.ts similarity index 100% rename from src/utils/journal/use-entry.ts rename to src/utils/entry/use-entry.ts From 19d1119c070d0823a94ed21bc193d300c8ab9bcd Mon Sep 17 00:00:00 2001 From: 273* Date: Sun, 10 May 2026 19:35:52 +0900 Subject: [PATCH 16/23] =?UTF-8?q?refactor:=20=E3=82=A8=E3=83=B3=E3=83=88?= =?UTF-8?q?=E3=83=AA=E3=83=BC=E8=A9=B3=E7=B4=B0=E3=82=92=E3=83=AA=E3=83=95?= =?UTF-8?q?=E3=82=A1=E3=82=AF=E3=82=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/entry/entry-detail.tsx | 25 ++++------------------- src/components/entry/entry-field-item.tsx | 19 ++++++++++++----- 2 files changed, 18 insertions(+), 26 deletions(-) diff --git a/src/components/entry/entry-detail.tsx b/src/components/entry/entry-detail.tsx index 53801a9..f05d6f3 100644 --- a/src/components/entry/entry-detail.tsx +++ b/src/components/entry/entry-detail.tsx @@ -8,18 +8,14 @@ import { Section, Spacer, Text, - VStack, } from "@expo/ui/swift-ui"; -import { - font, - foregroundStyle, - frame, - listStyle, -} from "@expo/ui/swift-ui/modifiers"; +import { frame, listStyle } from "@expo/ui/swift-ui/modifiers"; import { EntryDetailObj } from "@/db/queries/entries"; import { formatDate } from "@/utils/date"; +import { EntryFieldItem } from "./entry-field-item"; + type Props = { /** エントリーデータ */ entry: EntryDetailObj; @@ -64,20 +60,7 @@ export function EntryDetailView({ entry }: Props) { } > {sorted.map((v) => ( - - - {v.field.label} - - {v.value ?? ""} - + ))} diff --git a/src/components/entry/entry-field-item.tsx b/src/components/entry/entry-field-item.tsx index 82f4670..b7161df 100644 --- a/src/components/entry/entry-field-item.tsx +++ b/src/components/entry/entry-field-item.tsx @@ -15,19 +15,28 @@ type Props = { field: FieldlObj; /** 現在の値 */ value: FieldValue; - /** 値変更時のコールバック */ - setValue: (id: string, value: FieldValue) => void; + /** 値変更時のコールバック(edit=true のときのみ必要) */ + setValue?: (id: string, value: FieldValue) => void; + /** 入力かどうか */ + edit?: boolean; }; /** * フィールドタイプによってコンポーネントを切り替えるコンポーネント * React Compiler がこの単位で再レンダリングを最適化する */ -export function EntryFieldItem({ field, value, setValue }: Props) { +export function EntryFieldItem({ + field, + value, + setValue, + edit = false, +}: Props) { const shared = { label: field.label, - onValueChange: (v: T) => setValue(field.id, v), - edit: true, + onValueChange: setValue + ? (v: T) => setValue(field.id, v) + : undefined, + edit, }; switch (field.type as FieldType) { From c1701f304c9b06a3c3807cfb171cd9f4e8e3c8ae Mon Sep 17 00:00:00 2001 From: 273* Date: Sun, 10 May 2026 21:54:25 +0900 Subject: [PATCH 17/23] =?UTF-8?q?refactor:=20=E3=83=95=E3=82=A3=E3=83=BC?= =?UTF-8?q?=E3=83=AB=E3=83=89=E3=81=8C=E5=86=8D=E3=83=AC=E3=83=B3=E3=83=80?= =?UTF-8?q?=E3=83=AA=E3=83=B3=E3=82=B0=E3=81=95=E3=82=8C=E3=81=AA=E3=81=84?= =?UTF-8?q?=E3=82=88=E3=81=86=E3=83=AA=E3=83=95=E3=82=A1=E3=82=AF=E3=82=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(journal)/entry/create.tsx | 10 ++++-- src/components/entry/entry-create-view.tsx | 14 ++++---- src/utils/entry/use-entry.ts | 40 +++++++++++++++++----- 3 files changed, 47 insertions(+), 17 deletions(-) diff --git a/src/app/(journal)/entry/create.tsx b/src/app/(journal)/entry/create.tsx index 090dda1..4b81b17 100644 --- a/src/app/(journal)/entry/create.tsx +++ b/src/app/(journal)/entry/create.tsx @@ -6,10 +6,13 @@ import { View, } from "react-native"; +import { useLiveQuery } from "drizzle-orm/expo-sqlite"; import { Stack, useLocalSearchParams } from "expo-router"; import { SymbolView } from "expo-symbols"; import { EntryCreateView } from "@/components/entry/entry-create-view"; +import { getFieldsQuery } from "@/db/queries/fields"; +import { useEntry } from "@/utils/entry/use-entry"; const formDisabled = false; @@ -21,9 +24,12 @@ export default function EntryCreateScreen() { const { id, name } = useLocalSearchParams<{ id: string; name: string }>(); + const { data: fields } = useLiveQuery(getFieldsQuery(id)); + const { valuesRef, setValue, createEntry } = useEntry(fields); + const handleEntryCreate = async () => { // const { id } = await createJournal(); - // // replace でスタックせずにジャーナル詳細画面からジャーナル一覧へ戻れるようにする + createEntry(); // router.replace(`/(journal)/entry/${id}}`); }; @@ -51,7 +57,7 @@ export default function EntryCreateScreen() { ), }} /> - + ); } diff --git a/src/components/entry/entry-create-view.tsx b/src/components/entry/entry-create-view.tsx index 8f1c089..f754f43 100644 --- a/src/components/entry/entry-create-view.tsx +++ b/src/components/entry/entry-create-view.tsx @@ -6,27 +6,28 @@ import { useLiveQuery } from "drizzle-orm/expo-sqlite"; import { getFieldsQuery } from "@/db/queries/fields"; import { formatDate } from "@/utils/date"; -import { useEntry } from "@/utils/entry/use-entry"; +import { FieldValue } from "@/utils/entry/use-entry"; import { EntryFieldItem } from "./entry-field-item"; type Props = { - id: string; /** ジャーナル id */ + id: string; + /** 現在のフィールドの値 */ + values: Record; + /** フィールドに値を格納する関数 */ + setValue: (id: string, value: FieldValue) => void; }; /** * エントリー作成画面 */ -export function EntryCreateView({ id }: Props) { +export function EntryCreateView({ id, values, setValue }: Props) { const now = Date.now(); console.log(id); const { data: fields } = useLiveQuery(getFieldsQuery(id)); - const { values, setValue } = useEntry(fields); - if (!fields) return null; - return ( ))} diff --git a/src/utils/entry/use-entry.ts b/src/utils/entry/use-entry.ts index d7c96c8..06f61f4 100644 --- a/src/utils/entry/use-entry.ts +++ b/src/utils/entry/use-entry.ts @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useRef } from "react"; import type { FieldType } from "@/core/constants"; import { FieldlObj } from "@/db/schemas"; @@ -37,18 +37,40 @@ const getDefaultValue = (type: FieldType): FieldValue => { /** * エントリーフォームの値を管理するフック + * ref ベースのため、入力のたびに再レンダリングが発生しない * @param fields ジャーナルに紐づくフィールド一覧 * @returns - * - values 現在のフィールドの値 + * - valuesRef 現在のフィールドの値 * - setValue フィールドに値を格納する関数 + * - createEntry 新規エントリーをDBに保存する関数 */ -export const useEntry = (fields: FieldlObj[]) => { - const [values, setValues] = useState>(() => - Object.fromEntries(fields.map((f) => [f.id, getDefaultValue(f.type)])), - ); +export const useEntry = (fields: FieldlObj[] | undefined) => { + const valuesRef = useRef>({}); + const initialized = useRef(false); - const setValue = (id: string, value: FieldValue) => - setValues((prev) => ({ ...prev, [id]: value })); + // fields が初めてロードされたタイミングで一度だけ初期化 + if (!initialized.current && fields && fields.length > 0) { + initialized.current = true; + valuesRef.current = Object.fromEntries( + fields.map((f) => [f.id, getDefaultValue(f.type)]), + ); + } + + /** + * フィールドに値を格納する + * @param id フィールド id + * @param value フィールドの値 + */ + const setValue = (id: string, value: FieldValue): void => { + valuesRef.current[id] = value; + }; + + /** + * 新規エントリーをDBに保存する + */ + const createEntry = async () => { + console.log(valuesRef.current); + }; - return { values, setValue }; + return { valuesRef, setValue, createEntry }; }; From 8cfa07be394bb524e16d734d4f3e4523a0f76e53 Mon Sep 17 00:00:00 2001 From: 273* Date: Sun, 10 May 2026 23:34:26 +0900 Subject: [PATCH 18/23] =?UTF-8?q?feat:=20=E3=82=A8=E3=83=B3=E3=83=88?= =?UTF-8?q?=E3=83=AA=E3=83=BC=E7=99=BB=E9=8C=B2=E3=82=92=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(journal)/entry/create.tsx | 22 +++++++----- src/db/queries/entries.ts | 20 +++++++++++ src/db/schemas/entries.ts | 3 ++ src/utils/entry/use-entry.ts | 54 +++++++++++++++++++++++------- 4 files changed, 78 insertions(+), 21 deletions(-) diff --git a/src/app/(journal)/entry/create.tsx b/src/app/(journal)/entry/create.tsx index 4b81b17..dd9c820 100644 --- a/src/app/(journal)/entry/create.tsx +++ b/src/app/(journal)/entry/create.tsx @@ -7,7 +7,7 @@ import { } from "react-native"; import { useLiveQuery } from "drizzle-orm/expo-sqlite"; -import { Stack, useLocalSearchParams } from "expo-router"; +import { Stack, useLocalSearchParams, useRouter } from "expo-router"; import { SymbolView } from "expo-symbols"; import { EntryCreateView } from "@/components/entry/entry-create-view"; @@ -20,17 +20,19 @@ const formDisabled = false; * エントリー作成 */ export default function EntryCreateScreen() { - // const router = useRouter(); + const router = useRouter(); - const { id, name } = useLocalSearchParams<{ id: string; name: string }>(); + const { id: jounalId, name } = useLocalSearchParams<{ + id: string; + name: string; + }>(); - const { data: fields } = useLiveQuery(getFieldsQuery(id)); + const { data: fields } = useLiveQuery(getFieldsQuery(jounalId)); const { valuesRef, setValue, createEntry } = useEntry(fields); const handleEntryCreate = async () => { - // const { id } = await createJournal(); - createEntry(); - // router.replace(`/(journal)/entry/${id}}`); + const { id: newEntryId } = await createEntry(jounalId); + router.replace(`/(journal)/entry/${newEntryId}`); }; return ( @@ -57,7 +59,11 @@ export default function EntryCreateScreen() { ), }} /> - + ); } diff --git a/src/db/queries/entries.ts b/src/db/queries/entries.ts index 3ed79d0..3edb763 100644 --- a/src/db/queries/entries.ts +++ b/src/db/queries/entries.ts @@ -1,5 +1,7 @@ import { db } from "@/db/client"; +import { entries, entryValues, EntryObj, EntryValueObj } from "../schemas"; + /** * ジャーナルに紐付いたエントリー一覧を取得するクエリ * @param journalId ジャーナルID @@ -24,6 +26,24 @@ export const getEntryDetailQuery = (entryId: string) => }, }); +/** + * エントリーをフィールド値と共に保存する + * @param newEntry エントリー本体 + * @param newValues エントリー値一覧 + */ +export const storeEntry = async ( + newEntry: EntryObj, + newValues: EntryValueObj[], +): Promise => { + await db.transaction(async (tx) => { + await tx.insert(entries).values(newEntry); + + if (newValues.length > 0) { + await tx.insert(entryValues).values(newValues); + } + }); +}; + /** エントリー詳細の型 */ export type EntryDetailObj = NonNullable< Awaited> diff --git a/src/db/schemas/entries.ts b/src/db/schemas/entries.ts index aa8b8d0..65b0ec6 100644 --- a/src/db/schemas/entries.ts +++ b/src/db/schemas/entries.ts @@ -67,3 +67,6 @@ export const entryValuesRelations = relations(entryValues, ({ one }) => ({ references: [fields.id], }), })); + +export type EntryObj = typeof entries.$inferSelect; +export type EntryValueObj = typeof entryValues.$inferInsert; diff --git a/src/utils/entry/use-entry.ts b/src/utils/entry/use-entry.ts index 06f61f4..e5052a3 100644 --- a/src/utils/entry/use-entry.ts +++ b/src/utils/entry/use-entry.ts @@ -1,15 +1,10 @@ import { useRef } from "react"; -import type { FieldType } from "@/core/constants"; -import { FieldlObj } from "@/db/schemas"; +import * as Crypto from "expo-crypto"; -export type EntryObj = { - id: string; - date: Date; - title: string; - preview: string; - bookmark?: boolean; -}; +import type { FieldType } from "@/core/constants"; +import { storeEntry } from "@/db/queries/entries"; +import { EntryObj, EntryValueObj, FieldlObj } from "@/db/schemas"; export type FieldValue = string | number | boolean | Date | null; @@ -61,15 +56,48 @@ export const useEntry = (fields: FieldlObj[] | undefined) => { * @param id フィールド id * @param value フィールドの値 */ - const setValue = (id: string, value: FieldValue): void => { - valuesRef.current[id] = value; + const setValue = (fieldId: string, value: FieldValue): void => { + valuesRef.current[fieldId] = value; }; /** * 新規エントリーをDBに保存する + * @param journalId ジャーナル id + */ + /** FieldValue を DB の text 型に変換 */ + const serializeValue = (value: FieldValue): string | null => { + if (value === null) return null; + if (value instanceof Date) return String(value.getTime()); + return String(value); + }; + + /** + * 新規エントリーを値と共にDBに保存する + * @param journalId ジャーナル id */ - const createEntry = async () => { - console.log(valuesRef.current); + const createEntry = async (journalId: string) => { + const now = Date.now(); + + const newEntry: EntryObj = { + id: Crypto.randomUUID(), + journalId, + bookmark: false, + createdAt: now, + updatedAt: now, + }; + + const newValues: EntryValueObj[] = Object.entries(valuesRef.current).map( + ([fieldId, value]) => ({ + id: Crypto.randomUUID(), + entryId: newEntry.id, + fieldId, + value: serializeValue(value), + }), + ); + + await storeEntry(newEntry, newValues); + + return newEntry; }; return { valuesRef, setValue, createEntry }; From f658d30057091fc4e8b1422d3fb30b47b13586df Mon Sep 17 00:00:00 2001 From: 273* Date: Sun, 10 May 2026 23:50:26 +0900 Subject: [PATCH 19/23] =?UTF-8?q?refactor:=20=E8=BB=BD=E5=BE=AE=E3=81=AA?= =?UTF-8?q?=E3=83=AA=E3=83=95=E3=82=A1=E3=82=AF=E3=82=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(journal)/create.tsx | 4 ++-- src/components/entry/entry-create-view.tsx | 1 - src/db/queries/journals.ts | 10 +++++----- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/app/(journal)/create.tsx b/src/app/(journal)/create.tsx index c7c1b92..4f14fd0 100644 --- a/src/app/(journal)/create.tsx +++ b/src/app/(journal)/create.tsx @@ -25,10 +25,10 @@ export default function JournalCreateScreen() { } = useJournalField(); const handleJournalCreate = async () => { - const { id, name } = await createJournal(); + const { id: newJournalId, name } = await createJournal(); // replace でスタックせずにジャーナル詳細画面からジャーナル一覧へ戻れるようにする - router.replace(`/(journal)/${id}?name=${name}`); + router.replace(`/(journal)/${newJournalId}?name=${name}`); }; return ( diff --git a/src/components/entry/entry-create-view.tsx b/src/components/entry/entry-create-view.tsx index f754f43..46babc5 100644 --- a/src/components/entry/entry-create-view.tsx +++ b/src/components/entry/entry-create-view.tsx @@ -24,7 +24,6 @@ type Props = { */ export function EntryCreateView({ id, values, setValue }: Props) { const now = Date.now(); - console.log(id); const { data: fields } = useLiveQuery(getFieldsQuery(id)); diff --git a/src/db/queries/journals.ts b/src/db/queries/journals.ts index 52193c6..47348c4 100644 --- a/src/db/queries/journals.ts +++ b/src/db/queries/journals.ts @@ -18,21 +18,21 @@ export const getJournalsQuery = db.query.journals.findMany({ /** * ジャーナルをフィールドと共に作成するクエリを実行 - * @param journal ジャーナルのメタ情報 + * @param newJournal ジャーナルのメタ情報 * @param newFields フィールド一覧 */ export const storeJournal = async ( - journal: JournalObj, + newJournal: JournalObj, newFields: FieldWithSortObj[], -) => { +): Promise => { await db.transaction(async (tx) => { - await tx.insert(journals).values(journal); + await tx.insert(journals).values(newJournal); if (newFields.length > 0) { await tx .insert(fields) .values( - newFields.map((field) => ({ ...field, journalId: journal.id })), + newFields.map((field) => ({ ...field, journalId: newJournal.id })), ); } }); From 56d1628c65b6b3c18e25e7d727c0a5e85fb09818 Mon Sep 17 00:00:00 2001 From: 273* Date: Tue, 12 May 2026 00:50:15 +0900 Subject: [PATCH 20/23] =?UTF-8?q?feat:=20=E3=82=A8=E3=83=B3=E3=83=88?= =?UTF-8?q?=E3=83=AA=E3=83=BC=E4=B8=80=E8=A6=A7=E3=82=92=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/entry/entry-list-view.tsx | 59 +++-------- src/components/entry/entry-row.tsx | 8 +- src/db/queries/entries.ts | 12 +-- src/utils/entry/preview.ts | 122 +++++++++++++++++++++++ 4 files changed, 145 insertions(+), 56 deletions(-) create mode 100644 src/utils/entry/preview.ts diff --git a/src/components/entry/entry-list-view.tsx b/src/components/entry/entry-list-view.tsx index 3be2a00..ad90304 100644 --- a/src/components/entry/entry-list-view.tsx +++ b/src/components/entry/entry-list-view.tsx @@ -13,43 +13,14 @@ import { SymbolView } from "expo-symbols"; import type { SortKey } from "@/app/(journal)/[id]"; import { getEntriesQuery } from "@/db/queries/entries"; -import { formatYearMonth } from "@/utils/date"; -import { EntryObj } from "@/utils/entry/use-entry"; +import { + buildPreviewEntry, + groupByMonth, + sortEntries, +} from "@/utils/entry/preview"; import { EntryRow } from "./entry-row"; -function sortEntries(entries: EntryObj[], sortKey: SortKey): EntryObj[] { - switch (sortKey) { - case "dateDesc": - return [...entries].sort((a, b) => b.date.getTime() - a.date.getTime()); - case "dateAsc": - return [...entries].sort((a, b) => a.date.getTime() - b.date.getTime()); - case "titleAsc": - return [...entries].sort((a, b) => a.title.localeCompare(b.title)); - case "titleDesc": - return [...entries].sort((a, b) => b.title.localeCompare(a.title)); - case "bookmark": - return [...entries].sort( - (a, b) => (b.bookmark ? 1 : 0) - (a.bookmark ? 1 : 0), - ); - } -} - -function groupByMonth( - entries: EntryObj[], -): { month: string; entries: EntryObj[] }[] { - const map = new Map(); - for (const entry of entries) { - const key = `${entry.date.getFullYear()}-${entry.date.getMonth()}`; - if (!map.has(key)) map.set(key, []); - map.get(key)!.push(entry); - } - return Array.from(map.entries()).map(([, entries]) => ({ - month: formatYearMonth(entries[0].date), - entries, - })); -} - type Props = { /** ジャーナル id */ id: string; @@ -74,23 +45,19 @@ export function EntryListView({ const { data: dbEntries } = useLiveQuery(getEntriesQuery(id)); - const entries: EntryObj[] = (dbEntries ?? []).map((entry) => ({ - id: entry.id, - date: new Date(entry.createdAt), - title: entry.values[0]?.value ?? "", - preview: entry.values - .slice(1) - .map((v) => v.value) - .join(" "), - bookmark: entry.bookmark, - })); + const previewEntries = dbEntries.map(buildPreviewEntry); + // 検索 const filtered = searchText - ? entries.filter( + ? previewEntries.filter( (e) => e.title.includes(searchText) || e.preview.includes(searchText), ) - : entries; + : previewEntries; + + // 並び替え const sorted = sortEntries(filtered, sortKey); + + // 月毎にグループ分け const grouped = groupByMonth(sorted); return ( diff --git a/src/components/entry/entry-row.tsx b/src/components/entry/entry-row.tsx index ab67560..1aa3ea1 100644 --- a/src/components/entry/entry-row.tsx +++ b/src/components/entry/entry-row.tsx @@ -5,13 +5,13 @@ import { font, foregroundStyle, lineLimit } from "@expo/ui/swift-ui/modifiers"; import { useRouter } from "expo-router"; import { formatDate } from "@/utils/date"; -import { EntryObj } from "@/utils/entry/use-entry"; +import { PreviewEntryObj } from "@/utils/entry/preview"; const secondary = foregroundStyle({ type: "hierarchical", style: "secondary" }); type Props = { /** エントリーデータ */ - entry: EntryObj; + entry: PreviewEntryObj; }; /** @@ -28,12 +28,12 @@ export function EntryRow({ entry }: Props) { {entry.title} - + {entry.preview} - {formatDate(entry.date)} + {formatDate(entry.createdAt)} {entry.bookmark && ( diff --git a/src/db/queries/entries.ts b/src/db/queries/entries.ts index 3edb763..aaa2727 100644 --- a/src/db/queries/entries.ts +++ b/src/db/queries/entries.ts @@ -1,15 +1,15 @@ import { db } from "@/db/client"; -import { entries, entryValues, EntryObj, EntryValueObj } from "../schemas"; +import { entries, EntryObj, EntryValueObj, entryValues } from "../schemas"; /** - * ジャーナルに紐付いたエントリー一覧を取得するクエリ + * ジャーナルに紐付いたエントリー一覧をフィールドとともに取得するクエリ * @param journalId ジャーナルID */ export const getEntriesQuery = (journalId: string) => db.query.entries.findMany({ where: (entries, { eq }) => eq(entries.journalId, journalId), - with: { values: true }, + with: { values: { with: { field: true } } }, }); /** @@ -45,6 +45,6 @@ export const storeEntry = async ( }; /** エントリー詳細の型 */ -export type EntryDetailObj = NonNullable< - Awaited> ->; +export type EntryDetailObj = Awaited< + ReturnType +>[number]; diff --git a/src/utils/entry/preview.ts b/src/utils/entry/preview.ts new file mode 100644 index 0000000..500b0d0 --- /dev/null +++ b/src/utils/entry/preview.ts @@ -0,0 +1,122 @@ +import { SortKey } from "@/app/(journal)/[id]"; +import { FieldType } from "@/core/constants"; +import { EntryDetailObj } from "@/db/queries/entries"; + +import { formatDate, formatTime, formatYearMonth } from "../date"; + +export type PreviewEntryObj = { + /** エントリー id */ + id: string; + /** タイトル */ + title: string; + /** プレビュー */ + preview: string; + /** ブックマーク */ + bookmark: boolean; + /** 日付 */ + createdAt: Date; +}; + +/** + * DB の value 文字列をフィールド型に応じた表示文字列に変換 + * @param value 値 + * @param type フィールドタイプ + */ +export const formatFieldValue = ( + value: string | null, + type: FieldType, +): string => { + if (!value) return ""; + switch (type) { + case "date": + return formatDate(new Date(Number(value))); + case "time": + return formatTime(new Date(Number(value))); + case "check": + return value === "true" ? "✓" : ""; + default: + return value; + } +}; + +/** + * DB のエントリーをプレビュー表示用に変換する + * @param entry エントリー詳細 + */ +export const buildPreviewEntry = (entry: EntryDetailObj): PreviewEntryObj => { + const { id, createdAt, bookmark, values } = entry; + + // エントリー順に並び替え + const sorted = [...values].sort( + (a, b) => a.field.sortOrder - b.field.sortOrder, + ); + + // 1つめのフィールドをタイトルに設定する + const title = formatFieldValue( + sorted[0]?.value ?? null, + sorted[0]?.field.type, + ); + + // プレビューは2つ目以降のフィールドを順に並べる + const preview = sorted + .slice(1) + .map((v) => formatFieldValue(v.value, v.field.type as FieldType)) + .filter(Boolean) + .join(" "); + + return { + id, + createdAt: new Date(createdAt), + title, + preview, + bookmark, + }; +}; + +/** + * エントリーを並び替える + * @param previewEntries プレビューエントリー一覧 + * @param sortKey ソートキー + */ +export const sortEntries = ( + previewEntries: PreviewEntryObj[], + sortKey: SortKey, +): PreviewEntryObj[] => { + switch (sortKey) { + case "dateDesc": + return [...previewEntries].sort( + (a, b) => b.createdAt.getTime() - a.createdAt.getTime(), + ); + case "dateAsc": + return [...previewEntries].sort( + (a, b) => a.createdAt.getTime() - b.createdAt.getTime(), + ); + case "titleAsc": + return [...previewEntries].sort((a, b) => a.title.localeCompare(b.title)); + case "titleDesc": + return [...previewEntries].sort((a, b) => b.title.localeCompare(a.title)); + case "bookmark": + return [...previewEntries].sort( + (a, b) => (b.bookmark ? 1 : 0) - (a.bookmark ? 1 : 0), + ); + } +}; + +/** + * 月毎にエントリー一覧を分割する関数 + * @param previewEntries プレビューエントリー一覧 + */ +export const groupByMonth = ( + entries: PreviewEntryObj[], +): { month: string; entries: PreviewEntryObj[] }[] => { + const map = new Map(); + for (const entry of entries) { + const key = `${entry.createdAt.getFullYear()}-${entry.createdAt.getMonth()}`; + if (!map.has(key)) map.set(key, []); + map.get(key)!.push(entry); + } + return Array.from(map.entries()).map(([, entries]) => ({ + month: formatYearMonth(entries[0].createdAt), + entries, + })); +}; From 780217554b20f90c566e68cf367887c3a1d324a6 Mon Sep 17 00:00:00 2001 From: 273* Date: Tue, 12 May 2026 00:57:47 +0900 Subject: [PATCH 21/23] =?UTF-8?q?fix:=20=E8=BB=BD=E5=BE=AE=E3=81=AA?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/entry/entry-list-view.tsx | 7 ++++--- src/utils/entry/preview.ts | 14 +++++++------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/components/entry/entry-list-view.tsx b/src/components/entry/entry-list-view.tsx index ad90304..8170b49 100644 --- a/src/components/entry/entry-list-view.tsx +++ b/src/components/entry/entry-list-view.tsx @@ -45,6 +45,7 @@ export function EntryListView({ const { data: dbEntries } = useLiveQuery(getEntriesQuery(id)); + // エントリープレビュー一覧に変換 const previewEntries = dbEntries.map(buildPreviewEntry); // 検索 @@ -68,14 +69,14 @@ export function EntryListView({ > - {grouped.map(({ month, entries }) => ( + {grouped.map(({ month, previewEntries }) => (
- {entries.map((entry) => ( - + {previewEntries.map((previewEntry) => ( + ))}
))} diff --git a/src/utils/entry/preview.ts b/src/utils/entry/preview.ts index 500b0d0..c227216 100644 --- a/src/utils/entry/preview.ts +++ b/src/utils/entry/preview.ts @@ -46,7 +46,7 @@ export const formatFieldValue = ( export const buildPreviewEntry = (entry: EntryDetailObj): PreviewEntryObj => { const { id, createdAt, bookmark, values } = entry; - // エントリー順に並び替え + // フィールド順に並び替え const sorted = [...values].sort( (a, b) => a.field.sortOrder - b.field.sortOrder, ); @@ -107,16 +107,16 @@ export const sortEntries = ( * @param previewEntries プレビューエントリー一覧 */ export const groupByMonth = ( - entries: PreviewEntryObj[], -): { month: string; entries: PreviewEntryObj[] }[] => { + previewEntries: PreviewEntryObj[], +): { month: string; previewEntries: PreviewEntryObj[] }[] => { const map = new Map(); - for (const entry of entries) { + for (const entry of previewEntries) { const key = `${entry.createdAt.getFullYear()}-${entry.createdAt.getMonth()}`; if (!map.has(key)) map.set(key, []); map.get(key)!.push(entry); } - return Array.from(map.entries()).map(([, entries]) => ({ - month: formatYearMonth(entries[0].createdAt), - entries, + return Array.from(map.entries()).map(([, previewEntries]) => ({ + month: formatYearMonth(previewEntries[0].createdAt), + previewEntries, })); }; From f8263f077780ddb86f0186cbc7a4810d018fda90 Mon Sep 17 00:00:00 2001 From: 273* Date: Tue, 12 May 2026 21:56:24 +0900 Subject: [PATCH 22/23] =?UTF-8?q?fix:=20=E8=BB=BD=E5=BE=AE=E3=81=AA?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(journal)/entry/create.tsx | 4 +--- src/db/queries/entries.ts | 6 +++++- src/utils/entry/preview.ts | 7 +++++-- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/app/(journal)/entry/create.tsx b/src/app/(journal)/entry/create.tsx index dd9c820..4267ade 100644 --- a/src/app/(journal)/entry/create.tsx +++ b/src/app/(journal)/entry/create.tsx @@ -14,8 +14,6 @@ import { EntryCreateView } from "@/components/entry/entry-create-view"; import { getFieldsQuery } from "@/db/queries/fields"; import { useEntry } from "@/utils/entry/use-entry"; -const formDisabled = false; - /** * エントリー作成 */ @@ -28,7 +26,7 @@ export default function EntryCreateScreen() { }>(); const { data: fields } = useLiveQuery(getFieldsQuery(jounalId)); - const { valuesRef, setValue, createEntry } = useEntry(fields); + const { valuesRef, setValue, createEntry, formDisabled } = useEntry(fields); const handleEntryCreate = async () => { const { id: newEntryId } = await createEntry(jounalId); diff --git a/src/db/queries/entries.ts b/src/db/queries/entries.ts index aaa2727..191d4ba 100644 --- a/src/db/queries/entries.ts +++ b/src/db/queries/entries.ts @@ -9,7 +9,11 @@ import { entries, EntryObj, EntryValueObj, entryValues } from "../schemas"; export const getEntriesQuery = (journalId: string) => db.query.entries.findMany({ where: (entries, { eq }) => eq(entries.journalId, journalId), - with: { values: { with: { field: true } } }, + with: { + values: { + with: { field: true }, + }, + }, }); /** diff --git a/src/utils/entry/preview.ts b/src/utils/entry/preview.ts index c227216..73b0f8b 100644 --- a/src/utils/entry/preview.ts +++ b/src/utils/entry/preview.ts @@ -60,9 +60,9 @@ export const buildPreviewEntry = (entry: EntryDetailObj): PreviewEntryObj => { // プレビューは2つ目以降のフィールドを順に並べる const preview = sorted .slice(1) - .map((v) => formatFieldValue(v.value, v.field.type as FieldType)) + .map((v) => formatFieldValue(v.value, v.field.type)) .filter(Boolean) - .join(" "); + .join(" "); // 全ての値を繋げることで検索できる return { id, @@ -110,11 +110,14 @@ export const groupByMonth = ( previewEntries: PreviewEntryObj[], ): { month: string; previewEntries: PreviewEntryObj[] }[] => { const map = new Map(); + for (const entry of previewEntries) { const key = `${entry.createdAt.getFullYear()}-${entry.createdAt.getMonth()}`; + if (!map.has(key)) map.set(key, []); map.get(key)!.push(entry); } + return Array.from(map.entries()).map(([, previewEntries]) => ({ month: formatYearMonth(previewEntries[0].createdAt), previewEntries, From 25ef47645b920b5e94e649597b23503c8cc2eeb7 Mon Sep 17 00:00:00 2001 From: 273* Date: Tue, 12 May 2026 22:26:49 +0900 Subject: [PATCH 23/23] =?UTF-8?q?feat:=20=E3=82=A8=E3=83=B3=E3=83=88?= =?UTF-8?q?=E3=83=AA=E3=83=BC=E3=81=8C=E7=A9=BA=E3=81=AE=E6=99=82=E3=81=AE?= =?UTF-8?q?=E3=81=AF=E6=97=A5=E4=BB=98=E3=82=92=E8=A1=A8=E7=A4=BA=E3=81=97?= =?UTF-8?q?=E3=80=81=E4=BD=9C=E6=88=90=E3=83=9C=E3=82=BF=E3=83=B3=E3=81=AF?= =?UTF-8?q?=E5=B8=B8=E3=81=AB=E6=B4=BB=E6=80=A7=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(journal)/entry/create.tsx | 11 ++++------- src/utils/entry/preview.ts | 10 +++++----- src/utils/entry/use-entry.ts | 2 +- 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/src/app/(journal)/entry/create.tsx b/src/app/(journal)/entry/create.tsx index 4267ade..c6e303c 100644 --- a/src/app/(journal)/entry/create.tsx +++ b/src/app/(journal)/entry/create.tsx @@ -26,7 +26,7 @@ export default function EntryCreateScreen() { }>(); const { data: fields } = useLiveQuery(getFieldsQuery(jounalId)); - const { valuesRef, setValue, createEntry, formDisabled } = useEntry(fields); + const { valuesRef, setValue, createEntry } = useEntry(fields); const handleEntryCreate = async () => { const { id: newEntryId } = await createEntry(jounalId); @@ -44,14 +44,11 @@ export default function EntryCreateScreen() {
), headerRight: () => ( - + // エントリー作成では disabled は設定しない + ), diff --git a/src/utils/entry/preview.ts b/src/utils/entry/preview.ts index 73b0f8b..c50b935 100644 --- a/src/utils/entry/preview.ts +++ b/src/utils/entry/preview.ts @@ -51,11 +51,11 @@ export const buildPreviewEntry = (entry: EntryDetailObj): PreviewEntryObj => { (a, b) => a.field.sortOrder - b.field.sortOrder, ); - // 1つめのフィールドをタイトルに設定する - const title = formatFieldValue( - sorted[0]?.value ?? null, - sorted[0]?.field.type, - ); + // 1つめのフィールドをタイトルに設定し、値がなければ作成日にフォールバック + const titleFromField = sorted[0] + ? formatFieldValue(sorted[0].value, sorted[0].field.type as FieldType) + : ""; + const title = titleFromField || formatDate(new Date(createdAt)); // プレビューは2つ目以降のフィールドを順に並べる const preview = sorted diff --git a/src/utils/entry/use-entry.ts b/src/utils/entry/use-entry.ts index e5052a3..f9dee2a 100644 --- a/src/utils/entry/use-entry.ts +++ b/src/utils/entry/use-entry.ts @@ -75,7 +75,7 @@ export const useEntry = (fields: FieldlObj[] | undefined) => { * 新規エントリーを値と共にDBに保存する * @param journalId ジャーナル id */ - const createEntry = async (journalId: string) => { + const createEntry = async (journalId: string): Promise => { const now = Date.now(); const newEntry: EntryObj = {