Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
dcf785e
chore: claude.mdを更新
273Do May 17, 2026
792f113
feat: insights画面を実装
273Do May 19, 2026
35fb4d3
feat: journal選択チップを実装
273Do May 19, 2026
ce1093e
feat: insights画面のカードを実装
273Do May 19, 2026
cfb42b0
feat: インサイトカードのデモを実装
273Do May 19, 2026
e328dc8
feat: journal chipを画面上部に固定
273Do May 20, 2026
31acff7
feat: journal chipを画面上部に固定
273Do May 20, 2026
b2d5e8e
fix: 軽微なスタイルの修正
273Do May 20, 2026
cd8ce45
fix: jounral-chipのスタイルの修正
273Do May 20, 2026
2e75774
fix: カードの軽微な修正
273Do May 20, 2026
135a863
fix: 各種作成画面のボタンスタイルの修正
273Do May 20, 2026
5304812
refactor: 軽微なリファクタ
273Do May 20, 2026
2e4e075
refactor: 軽微なリファクタ
273Do May 20, 2026
74b9701
feat: 不要なコンポーネントを削除
273Do May 21, 2026
d0e9d4d
refactor: コンポーネントを分離
273Do May 21, 2026
28432fb
feat: textフィールドの統計を実装しスタイルを調整
273Do May 21, 2026
f7667f6
feat: チャートスタイルを調整
273Do May 21, 2026
d958213
chore: Merge branch 'develop' into feature/#16-screen-mock-insite
273Do May 21, 2026
10ae140
chore: eslintに設定を追加
273Do May 23, 2026
09b3031
refactor: 型エラーが起きないようリファクタ
273Do May 23, 2026
98b4cea
feat: longTextの統計カードを実装
273Do May 23, 2026
6f20971
fix: スタイルなどの軽微な修正
273Do May 24, 2026
3a066aa
feat: そのほかのfield stats cardを実装
273Do May 24, 2026
2f15f3a
feat: jsdocを追加
273Do May 24, 2026
3c571a3
fix: 軽微な修正
273Do May 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 63 additions & 8 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ Nicky is a React Native journaling app built with Expo and Expo Router. It featu
## Development Commands

```bash
pnpm expo run:ios # Build and run on iOS simulator
pnpm expo start --clear # Start Expo dev server (clear cache)
pnpm lint # Run ESLint
pnpm lint-fix # Run ESLint with auto-fix
pnpm typecheck # TypeScript type check
pnpm expo run:ios # Build and run on iOS simulator
pnpm expo start --clear # Start Expo dev server (clear cache)
pnpm lint # Run ESLint
pnpm lint-fix # Run ESLint with auto-fix
pnpm typecheck # TypeScript type check
pnpm drizzle-kit generate # Generate migration files from schema
```

Expand All @@ -39,9 +39,11 @@ src/app/
_layout.tsx # Stack — scoped to journal tab
index.tsx # Journal list /
create.tsx # Create journal /create
edit.tsx # Edit journal /edit?journalId=...
[id].tsx # Entry list /[id]
entry/
[id].tsx # Entry detail /entry/[id]
create.tsx # Create entry /entry/create?journalId=...&journalName=...
[id].tsx # Entry detail /entry/[id]?journalName=...
explore.tsx # Report tab
```

Expand All @@ -66,14 +68,65 @@ Drizzle ORM + expo-sqlite. Schema is split by domain in `src/db/schemas/`:
**Drizzle config notes:**
- Schema files must not import React Native packages (`expo-crypto`, `expo-symbols` runtime imports) — drizzle-kit runs in Node.js. Use `import type` for RN types.
- `$defaultFn` with `Crypto.randomUUID()` cannot be used in schema — generate IDs at the application layer instead.
- SQLite column names in Drizzle are **camelCase** (e.g. `sortOrder`, not `sort_order`). When writing raw SQL in `onConflictDoUpdate`, quote them: `excluded."sortOrder"`.

### `useLiveQuery` Reactivity Rules

`useLiveQuery` from `drizzle-orm/expo-sqlite` **only watches the root table** of a query — it does NOT automatically detect changes to tables joined via `with:`. The second argument is just `useEffect` deps, not additional table watchers.

**Pattern used in this codebase:** When a transaction modifies a related table (e.g. `fields`), also `touch` the root-table row so `useLiveQuery` re-runs:

```ts
// In updateJournal — touch entries.updatedAt so entry-list useLiveQuery detects the change
await tx.update(entries).set({ updatedAt: Date.now() }).where(eq(entries.journalId, journalId));
```

This means:
- `getEntriesQuery` (`entries` root) re-runs after any `fields` or `entry_values` change because those transactions touch `entries.updatedAt`
- `getFieldsQuery` (`fields` root) re-runs directly when `fields` is written — no touch needed

### Sub-component Pattern for Forms with `useRef` State

Hooks that use `useRef` + an `initialized` flag (e.g. `useEntry`, `useJournalField`) only initialize once per mount. To ensure they pick up fresh data when switching to edit mode, **mount the form as a separate child component** rather than toggling visibility on the parent:

```tsx
// ✅ Correct — EntryEditForm mounts fresh each time editMode becomes true
{editMode && <EntryEditForm entry={entry} onSave={...} />}

// ❌ Wrong — form is always mounted; useRef init won't re-run with new data
<EntryCreateView ... /> // toggled visible/hidden
```

This pattern is used in `entry/[id].tsx` (`EntryEditForm`) and `edit.tsx` (`JournalEditForm`).

### Field Value Serialization

All field values are stored as `text | null` in `entry_values.value`. Serialization/deserialization lives in `src/utils/entry/use-entry.ts`:
- `serializeValue(FieldValue) → string | null` — converts to DB text
- `deserializeValue(string | null, FieldType) → FieldValue` — converts from DB text
- `FieldType` values: `"text" | "longText" | "number" | "media" | "check" | "date" | "time" | "location"`
- Dates/times are stored as millisecond timestamps (`String(date.getTime())`)

`buildEntryFormData` in `src/utils/entry/entry-form.ts` extracts fields (sorted by `sortOrder`) and initial values from an `EntryDetailObj`.

### Entry Preview Pipeline

`EntryDetailObj` (raw DB join) → `PreviewEntryObj` (display) via `buildPreviewEntry` in `src/utils/entry/preview.ts`:
- First field value = title (falls back to formatted `createdAt`)
- Remaining field values joined with spaces = preview (also used for search)
- Entries grouped by month for the list view (`groupByMonth`)

### Keyboard Dismiss Rule

Always call `Keyboard.dismiss()` **before** any `async` save operation that triggers navigation. Skipping this causes a `RemoteTextInput` session crash on iOS when the keyboard is mid-input as the screen unmounts.

### Key Technologies

| Package | Usage |
|---|---|
| `expo-router` | File-based routing, `useRouter`, `useLocalSearchParams` |
| `@expo/ui/swift-ui` | SwiftUI components: `Host`, `ZStack`, `VStack`, `Grid`, `ScrollView`, `List`, `Section`, `Button`, `Image`, `Text`, `RoundedRectangle`, `ColorPicker`, `BottomSheet` |
| `@expo/ui/swift-ui/modifiers` | `frame`, `padding`, `foregroundStyle`, `onTapGesture`, `listStyle`, `presentationDetents`, `environment`, `fixedSize` |
| `@expo/ui/swift-ui` | SwiftUI components: `Host`, `ZStack`, `VStack`, `HStack`, `Spacer`, `Grid`, `ScrollView`, `List`, `Section`, `Button`, `Image`, `Text`, `RoundedRectangle`, `ColorPicker`, `BottomSheet`, `ContextMenu` |
| `@expo/ui/swift-ui/modifiers` | `frame`, `padding`, `foregroundStyle`, `onTapGesture`, `listStyle`, `presentationDetents`, `environment`, `fixedSize`, `font`, `lineLimit` |
| `expo-router/unstable-native-tabs` | `NativeTabs` — iOS native tab bar |
| `expo-symbols` | `SymbolView` — SF Symbols in RN (non-SwiftUI) header components |
| `expo-sqlite` + `drizzle-orm` | Local SQLite persistence |
Expand All @@ -89,6 +142,7 @@ Drizzle ORM + expo-sqlite. Schema is split by domain in `src/db/schemas/`:
- Adaptive colors: use `PlatformColor("label")` directly — no need for `useColorScheme`
- `fixedSize()` on a component prevents it from stretching in an HStack, allowing siblings to fill remaining space
- Gradients: `RoundedRectangle` + `foregroundStyle({ type: "linearGradient", ... })` + `clipShape` on parent `ZStack`
- Native navigation bar buttons: use `unstable_headerRightItems` on `Stack.Screen` (not `headerRight` + RN `Pressable`)

### Naming Conventions

Expand All @@ -104,3 +158,4 @@ Drizzle ORM + expo-sqlite. Schema is split by domain in `src/db/schemas/`:
- **TypeScript:** strict mode enabled
- **Formatter:** Prettier (enforced via ESLint)
- **React Compiler:** enabled — do not manually add `useMemo`/`useCallback` unless there is a specific reason
- **Event handler note:** extract `e.nativeEvent.text` synchronously before passing to async state updaters (React synthetic event pooling)
10 changes: 10 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,16 @@ module.exports = defineConfig([
"unused-imports": unusedImports,
},
rules: {
// const 以外の型アサーションを禁止
"no-restricted-syntax": [
"error",
{
selector:
"TSAsExpression:not([typeAnnotation.typeName.name='const'])",
message:
"Type assertions using 'as' are not allowed. Use type guards or proper type definitions instead ('as const' is permitted).",
},
],
// Import/未使用変数
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": "off",
Expand Down
1 change: 1 addition & 0 deletions src/app/(journal)/create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export default function JournalCreateScreen() {
tintColor: formDisabled
? PlatformColor("tertiaryLabel")
: PlatformColor("systemIndigo"),
variant: formDisabled ? undefined : "prominent",
disabled: formDisabled,
onPress: formDisabled ? () => {} : handleJournalCreate,
},
Expand Down
1 change: 1 addition & 0 deletions src/app/(journal)/edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ function JournalEditForm({ journal }: FormProps) {
tintColor: formDisabled
? PlatformColor("tertiaryLabel")
: PlatformColor("systemIndigo"),
variant: formDisabled ? undefined : "prominent",
disabled: formDisabled,
onPress: formDisabled ? () => {} : handleSave,
},
Expand Down
1 change: 1 addition & 0 deletions src/app/(journal)/entry/[id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export default function EntryDetailScreen() {
label: "Save",
icon: { type: "sfSymbol", name: "checkmark" },
tintColor: PlatformColor("systemIndigo"),
variant: "prominent",
onPress: handleSave,
},
]
Expand Down
1 change: 1 addition & 0 deletions src/app/(journal)/entry/create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export default function EntryCreateScreen() {
label: "Create New Entry",
icon: { type: "sfSymbol", name: "checkmark" },
tintColor: PlatformColor("systemIndigo"),
variant: "prominent",
onPress: handleEntryCreate,
},
],
Expand Down
2 changes: 0 additions & 2 deletions src/app/explore.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { PlatformColor, View } from "react-native";
import { Host, VStack } from "@expo/ui/swift-ui";

import { AllformList } from "@/components/all-form-list";
import ChartDemo from "@/components/chart-demo";

export default function ExploreScreen() {
return (
Expand All @@ -14,7 +13,6 @@ export default function ExploreScreen() {
>
<VStack>
<AllformList />
<ChartDemo />
</VStack>
</Host>
</View>
Expand Down
20 changes: 20 additions & 0 deletions src/app/insights/_layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { useColorScheme } from "react-native";

import { Stack } from "expo-router";

export default function InsightsLayout() {
const colorScheme = useColorScheme();
const titleColor = colorScheme === "dark" ? "#ffffff" : "#000000";

return (
<Stack
screenOptions={{
headerTransparent: true,
headerTitleStyle: {
color: titleColor,
},
headerBackButtonDisplayMode: "minimal",
}}
/>
);
}
31 changes: 31 additions & 0 deletions src/app/insights/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Stack } from "expo-router";

import { InsightsView } from "@/components/insights/insights-view";
Comment thread
273Do marked this conversation as resolved.

/**
* インサイト画面
*/
export default function InsightsScreen() {
// const router = useRouter();

return (
<>
<Stack.Screen
options={{
title: "Insights",
headerLargeTitleEnabled: true,
unstable_headerRightItems: () => [
{
type: "button",
label: "Create New Journal",
icon: { type: "sfSymbol", name: "gearshape" },
// onPress: () => router.push("/(journal)/create"),
onPress: () => console.log("test"),
},
],
}}
/>
<InsightsView />
</>
);
}
4 changes: 2 additions & 2 deletions src/components/app-tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ export default function AppTabs() {
<NativeTabs.Trigger.Icon sf="books.vertical.fill" />
</NativeTabs.Trigger>

<NativeTabs.Trigger name="explore">
<NativeTabs.Trigger.Label>Insite</NativeTabs.Trigger.Label>
<NativeTabs.Trigger name="insights">
<NativeTabs.Trigger.Label>Insights</NativeTabs.Trigger.Label>
<NativeTabs.Trigger.Icon sf="waveform.path.ecg" />
</NativeTabs.Trigger>
</NativeTabs>
Expand Down
74 changes: 0 additions & 74 deletions src/components/chart-demo.tsx

This file was deleted.

45 changes: 37 additions & 8 deletions src/components/entry/entry-field-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ 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 { FieldObj } from "@/db/schemas";
import { type FieldValue } from "@/utils/entry/use-entry";

Expand Down Expand Up @@ -39,19 +38,49 @@ export function EntryFieldItem({
edit,
};

switch (field.type as FieldType) {
switch (field.type) {
case "text":
return <EntryText {...shared} defaultValue={value as string} />;
return (
<EntryText
{...shared}
defaultValue={typeof value === "string" ? value : undefined}
/>
);
case "longText":
return <EntryLongText {...shared} defaultValue={value as string} />;
return (
<EntryLongText
{...shared}
defaultValue={typeof value === "string" ? value : undefined}
/>
);
case "number":
return <EntryNumber {...shared} defaultValue={(value ?? 0) as number} />;
return (
<EntryNumber
{...shared}
defaultValue={typeof value === "number" ? value : undefined}
/>
);
case "check":
return <EntryCheck {...shared} defaultValue={value as boolean} />;
return (
<EntryCheck
{...shared}
defaultValue={typeof value === "boolean" ? value : undefined}
/>
);
case "date":
return <EntryDate {...shared} defaultValue={value as Date} />;
return (
<EntryDate
{...shared}
defaultValue={value instanceof Date ? value : undefined}
/>
);
case "time":
return <EntryTime {...shared} defaultValue={value as Date} />;
return (
<EntryTime
{...shared}
defaultValue={value instanceof Date ? value : undefined}
/>
);
case "media":
return <EntryMedia {...shared} />;
case "location":
Expand Down
2 changes: 1 addition & 1 deletion src/components/entry/entry-list-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ export function EntryListView({
>
<GlassView
glassEffectStyle="regular"
tintColor={PlatformColor("systemGra1y3") as unknown as string}
tintColor={PlatformColor("systemGray3").toString()}
isInteractive
style={styles.glassButton}
>
Expand Down
Loading
Loading