Skip to content

Feat/cycling events - #12

Open
anu-mdl wants to merge 38 commits into
mainfrom
feat/cycling-events
Open

Feat/cycling events#12
anu-mdl wants to merge 38 commits into
mainfrom
feat/cycling-events

Conversation

@anu-mdl

@anu-mdl anu-mdl commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features

    • Added recurring series: create, manage meetings/members, join/leave, and show meeting schedules with attendance.
    • Added profile editing (photo picker, categories, socials) and local “my” saving (profile, likes, participation responses).
    • Added sharing, liking, and optional chat-link actions for activities/series.
  • Improvements

    • Home feed merges activities and series with city/text filtering and unified sorting.
    • Activity/series forms support chat links; capacity and participation status are displayed more clearly.
    • Manage pages now include share actions and improved meeting/member editing views.
  • Tests

    • Expanded unit test coverage for core validation, mappings, and sharing behavior.
  • Chores

    • Updated Expo permissions and navigation links.

anu-mdl and others added 30 commits June 28, 2026 18:05
Выбор фото профиля через системную галерею (локальный URI).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
readDeviceJson/writeDeviceJson/removeDeviceValue поверх булевых флагов —
slice'ы не дёргают AsyncStorage напрямую.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Аватар-плейсхолдер, мультивыбор интересов и выбор фото из галереи.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
readMyActivities/addMyActivity (дедуп по slug) + activityKeys.mine,
useMyActivities, useTrackMyActivity.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
readMyResponses/addMyResponse (дедуп по slug) + participantKeys.mine,
useMyResponses, useTrackMyResponse.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
types/schemas/utils/storage/hooks/mappers + unit-тесты. Профиль живёт в
AsyncStorage, без серверного аккаунта. 4 соцсети (telegram, instagram,
whatsapp, website), фото — локальный URI без URL-валидации.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…писки

На успешном создании/отклике дописываем в device-list через
useTrackMyActivity/useTrackMyResponse. Сбой записи не роняет операцию.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
useEditProfileForm (RHF + zodResolver + useSaveProfile, префилл через
mapProfileToForm) и EditProfileForm с выбором фото и интересов.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Просмотр профиля (фото, статистика, соцсети наружу, «Мои ивенты»),
экран редактирования, пустое состояние. Ссылка в шапку _layout.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
TESTS.md дополнен секциями profile (schemas/mappers/utils). В
about_the_project уточнены открытое создание ивентов и модерация.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Форма всегда даёт status «going» (литерал в схеме), выбор статуса убран.
participantStatusOptions больше не нужен. Статистика и список читают старые
записи maybe/cant. Тесты маппера и TESTS.md — payload всегда going.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Убран Select статуса из формы отклика — остаются имя, телеграм, комментарий.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Строка статистики на странице ивента — «Идут: N» вместо трёх статусов.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@anu-mdl
anu-mdl requested a review from lukivan8 July 7, 2026 19:46
@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 91d846c3-53be-4e81-b7d9-8fbfab4710ba

📥 Commits

Reviewing files that changed from the base of the PR and between c68e8a5 and dba6ab4.

📒 Files selected for processing (18)
  • app.json
  • app/a/[slug].tsx
  • app/index.tsx
  • app/manage/series/[editToken].tsx
  • src/entities/activity/local.test.ts
  • src/entities/activity/local.ts
  • src/entities/profile/schemas.ts
  • src/entities/series/api.ts
  • src/entities/series/hooks.ts
  • src/entities/series/schemas.ts
  • src/entities/series/utils.test.ts
  • src/features/create-series/RecurrenceFields.tsx
  • src/features/join-series/useJoinSeries.ts
  • src/features/like-activity/useLikeActivity.ts
  • src/features/manage-series/ManageSeriesForm.tsx
  • src/features/manage-series/useManageSeriesForm.ts
  • src/shared/lib/storage.ts
  • src/shared/ui/MultiSelect.tsx
✅ Files skipped from review due to trivial changes (2)
  • app.json
  • src/features/create-series/RecurrenceFields.tsx
🚧 Files skipped from review as they are similar to previous changes (13)
  • src/features/manage-series/ManageSeriesForm.tsx
  • app/a/[slug].tsx
  • src/entities/activity/local.test.ts
  • src/shared/ui/MultiSelect.tsx
  • src/features/manage-series/useManageSeriesForm.ts
  • src/entities/activity/local.ts
  • src/entities/series/api.ts
  • src/entities/series/utils.test.ts
  • app/manage/series/[editToken].tsx
  • app/index.tsx
  • src/features/join-series/useJoinSeries.ts
  • src/entities/series/schemas.ts
  • src/entities/series/hooks.ts

📝 Walkthrough

Walkthrough

This PR adds a recurring series domain, device-local profile storage/editing, activity chat/capacity/share/like support, participant simplification, merged home feeds, and updated docs/tests.

Changes

Shared utilities

Layer / File(s) Summary
Datetime helpers
src/shared/lib/datetime.ts, ...test.ts
Adds date validation and ISO/datetime-local conversion helpers.
Public link helpers
src/shared/lib/publicLink.ts, ...test.ts
Adds slug and edit-token generation helpers.
Shared Zod validators
src/shared/lib/zodFields.ts
Adds reusable optional text, capacity, URL, and chat URL validators.
Device storage helpers
src/shared/lib/storage.ts
Adds JSON read/write/update/remove helpers for device storage.
Sharing and public URLs
src/shared/lib/shareActivity.ts, publicUrl.ts, ShareButton.tsx, ...test.ts
Adds share/copy handling, series public URLs, and a share button UI.
Shared UI controls
src/shared/ui/Avatar.tsx, ChatLinkButton.tsx, PhotoPicker.tsx, MultiSelect.tsx, app.json, package.json
Adds avatar, chat-link, photo-picker, and multi-select UI, plus image-picker configuration.

Activity enhancements

Layer / File(s) Summary
Types and schema
src/entities/activity/types.ts, schemas.ts, ...test.ts
Adds chat_url to activity types and validation.
Mappers
src/entities/activity/mappers.ts, ...test.ts
Updates activity form/API mapping for chat_url and shared helpers.
Utility rewrite
src/entities/activity/utils.ts, ...test.ts
Replaces slug/date helpers with capacity, status, and Telegram URL helpers.
Listing and local tracking
src/entities/activity/api.ts, hooks.ts, local.ts, ...test.ts
Adds filtered listing plus local "mine" and like/tracking storage.
Forms and activity page
src/features/create-activity/*, src/features/manage-activity/*, app/a/[slug].tsx
Adds chat_url-aware create/manage forms and like/share/chat/capacity UI.

Participant simplification

Layer / File(s) Summary
Schema and mapper changes
src/entities/participant/schemas.ts, types.ts, mappers.ts, ...test.ts
Restricts status to "going" and removes comment handling.
Local tracking and response UI
src/entities/participant/local.ts, hooks.ts, src/features/respond-to-activity/*, src/shared/ui/ParticipantList.tsx
Adds local response tracking and removes status/comment fields from the response form.

Profile feature

Layer / File(s) Summary
Domain and storage
src/entities/profile/*
Adds profile types, schema, storage, hooks, mappers, and utilities.
Profile UI
src/shared/ui/Avatar.tsx, PhotoPicker.tsx, MultiSelect.tsx, app.json, package.json
Adds profile-editing UI and Expo image-picker support.
Edit and dashboard pages
app/profile/edit.tsx, app/profile/index.tsx, app/_layout.tsx
Adds the profile edit page, dashboard page, and navigation link.

Series feature

Layer / File(s) Summary
Domain, schemas, utils, API, hooks, local storage
src/entities/series/*
Adds recurrence types, validation, scheduling utilities, Supabase access, React Query hooks, and local storage.
Create/manage/join UI
src/features/create-series/*, src/features/manage-series/*, src/features/join-series/*, src/features/mark-meeting/*, app/manage/series/[editToken].tsx, app/s/[slug].tsx
Adds the series creation, management, joining, marking, and public series screens.

App shell

Layer / File(s) Summary
Create mode switch and merged feed
app/create.tsx, app/index.tsx
Adds activity/series mode selection and merges both entities into one sorted home feed.

Docs

Layer / File(s) Summary
Test catalog and product docs
TESTS.md, docs/about_the_project.md, docs/todo.md
Updates the test catalog and product documentation/backlog.

Estimated code review effort: 5 (Critical) | ~150 minutes

Possibly related PRs

  • linkerkz/project-e#6: Refines the same activity mapper and participant handling areas that this PR further extends with shared helpers, chat_url, and status simplification.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title is only loosely related to the PR and is too vague to convey the main change. Use a specific title that mentions the core work, such as recurring series/event creation, profiles, and sharing changes.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/cycling-events

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 15

🧹 Nitpick comments (13)
src/shared/lib/zodFields.ts (1)

12-41: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Deprecated z.string().url() chained API.

Per Zod v4 docs, z.string().url() is deprecated in favor of the top-level z.url() and will be removed in a future major version. Since this module is newly authored against zod 4.4.3, prefer the non-deprecated form.

♻️ Suggested update
 export const optionalUrl = z.preprocess(
   emptyToNull,
-  z.string().url("Введите корректную ссылку").nullable(),
+  z.url("Введите корректную ссылку").nullable(),
 );
...
 export const chatUrl = z.preprocess(
   emptyToNull,
   z
-    .string()
-    .url("Введите корректную ссылку")
+    .url("Введите корректную ссылку")
     .refine(
       isKnownChatHost,
       "Ссылка должна вести в Telegram, WhatsApp, Discord или Signal",
     )
     .nullable(),
 );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/shared/lib/zodFields.ts` around lines 12 - 41, The url validation in
optionalUrl and chatUrl still uses the deprecated z.string().url() chain, so
update both schemas to use the top-level z.url() API instead. Keep the existing
emptyToNull preprocessing and nullable behavior, and preserve the current error
message and isKnownChatHost refinement in chatUrl so the validation logic
remains unchanged.
src/shared/lib/publicLink.ts (1)

24-26: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Math.random() suffix is fine for de-duplication, not for security.

Since generateEditToken already uses expo-crypto, consider reusing a crypto-based random source for the slug suffix too for consistency, though this isn't security-sensitive.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/shared/lib/publicLink.ts` around lines 24 - 26, The slug suffix
generation in shortRandom currently uses Math.random(), which is acceptable for
deduplication but inconsistent with the crypto-based approach used by
generateEditToken. Update shortRandom in publicLink.ts to use the same
expo-crypto random source for generating the 6-character suffix, keeping the
helper’s behavior the same while aligning it with the rest of the token
generation logic.
src/shared/ui/ShareButton.tsx (1)

13-19: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

No guard against rapid repeated presses.

share() can be triggered multiple times before the previous call resolves (e.g. double-tap), potentially opening multiple native share sheets or firing duplicate clipboard writes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/shared/ui/ShareButton.tsx` around lines 13 - 19, The ShareButton share()
handler can be invoked multiple times before the previous request completes,
causing duplicate share actions. Update ShareButton to guard against re-entry in
share(), using the existing share() function and its state so rapid taps are
ignored until shareActivity({ slug, title }) resolves, then clear the guard
after setNotice(noticeFor(result)) runs.
src/shared/lib/storage.ts (1)

16-24: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider runtime validation of parsed JSON.

readDeviceJson casts JSON.parse(raw) directly to T with no shape validation. If stored data becomes stale/corrupted (e.g. after a schema change), consumers will silently receive malformed objects typed as valid T. Since this cohort already introduces Zod validators (zodFields.ts), consider accepting an optional schema to .safeParse the result and return null on mismatch.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/shared/lib/storage.ts` around lines 16 - 24, `readDeviceJson` currently
trusts `JSON.parse(raw)` and returns it as `T` without checking shape, so stale
or corrupted storage can flow through as if valid. Update `readDeviceJson` in
`storage.ts` to accept an optional Zod schema/validator (consistent with
`zodFields.ts`), parse the JSON, and run `.safeParse` before returning. If
parsing fails or the value does not match the schema, return `null`; keep the
existing `null` behavior for missing or invalid JSON.
src/shared/lib/shareActivity.test.ts (1)

1-45: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing test coverage for the web fallback path.

Only the native (Platform.OS: "ios") branch is tested here. Consider adding a separate describe block with Platform.OS: "web" to cover shouldCopyInstead/copyLink, including the navigator.clipboard unavailable and rejection paths.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/shared/lib/shareActivity.test.ts` around lines 1 - 45, Add test coverage
for the web fallback in shareActivity tests, since only the native Platform.OS:
"ios" path is covered now. Create a separate describe block that mocks
Platform.OS as "web" and exercises the shouldCopyInstead/copyLink flow,
including cases where navigator.clipboard is missing and where clipboard write
rejects. Keep the assertions focused on the shareActivity result shape and the
fallback behavior.
src/features/create-activity/useCreateActivityForm.ts (1)

63-67: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Silent tracking failures are unobservable.

The empty catch {} is a reasonable tradeoff to avoid blocking the create flow, but it discards all error information, making tracking failures invisible in production.

♻️ Add lightweight logging without changing behavior
   async function rememberCreated(activity: Activity) {
     try {
       await track.mutateAsync(activity);
-    } catch {}
+    } catch (error) {
+      console.warn("Не удалось сохранить активность в локальный список", error);
+    }
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/create-activity/useCreateActivityForm.ts` around lines 63 - 67,
The rememberCreated helper in useCreateActivityForm currently swallows
track.mutateAsync failures with an empty catch, making tracking issues
invisible. Keep the non-blocking behavior, but add lightweight logging in the
catch block (using the existing logger/error-reporting pattern in this feature
or nearby hooks) so failures are observable without affecting the create flow.
src/features/create-series/useCreateSeriesForm.ts (1)

4-4: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Series feature depends on activity entity for an unrelated utility.

telegramToChatUrl operates on profile/telegram data but lives in entities/activity/utils and is now imported by the series feature. This creates unnecessary cross-domain coupling: a future change to activity's utils module for activity-specific reasons risks breaking series form prefill too.

♻️ Relocate the helper
-import { telegramToChatUrl } from "../../entities/activity/utils";
+import { telegramToChatUrl } from "../../shared/lib/telegram";

Move the function body from entities/activity/utils.ts to a shared/profile-owned module and update both useCreateActivityForm.ts and useCreateSeriesForm.ts accordingly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/create-series/useCreateSeriesForm.ts` at line 4, The series form
is importing telegramToChatUrl from an activity-specific utils module, creating
unnecessary cross-domain coupling between series and activity. Move
telegramToChatUrl out of entities/activity/utils into a shared/profile-owned
module, then update both useCreateActivityForm and useCreateSeriesForm to import
it from the new location. Keep the helper’s behavior unchanged and ensure the
series feature no longer depends on activity-only utilities.
src/features/create-series/CreateSeriesForm.tsx (1)

17-68: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate field set with CreateActivityForm.tsx.

Title, description, city, location_text, capacity, cover_url, and chat_url fields (labels, error wiring) are copy-pasted verbatim from CreateActivityForm.tsx; only the starts_at label differs. This will drift as fields/validation messages evolve independently in each form.

♻️ Extract shared core fields
+// src/features/shared/EventCoreFields.tsx
+export function EventCoreFields({ control, errors, startsAtLabel }: {
+  control: Control<any>;
+  errors: FieldErrors<any>;
+  startsAtLabel: string;
+}) {
+  return (
+    <>
+      <FormField control={control} name="title" label="название, обязательно" error={errors.title?.message} />
+      <FormField control={control} name="description" label="описание" component={Textarea} error={errors.description?.message} />
+      <FormField control={control} name="city" label="город, обязательно" error={errors.city?.message} />
+      <FormField control={control} name="location_text" label="место текстом" error={errors.location_text?.message} />
+      <FormField control={control} name="starts_at" label={startsAtLabel} component={DateTimeInput} error={errors.starts_at?.message} />
+      <FormField control={control} name="capacity" label="лимит мест, число или пусто" error={errors.capacity?.message} />
+      <FormField control={control} name="cover_url" label="ссылка на обложку" error={errors.cover_url?.message} />
+      <FormField control={control} name="chat_url" label="ссылка на чат" error={errors.chat_url?.message} />
+    </>
+  );
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/create-series/CreateSeriesForm.tsx` around lines 17 - 68, The
form field block in CreateSeriesForm is duplicated from the shared activity
form, so it should be extracted into a reusable core fields component or helper
used by both CreateSeriesForm and CreateActivityForm. Move the shared FormField
wiring for title, description, city, location_text, capacity, cover_url, and
chat_url into a common component, and keep only the series-specific starts_at
label/behavior in CreateSeriesForm so future validation or label changes stay in
sync.
src/shared/ui/PhotoPicker.tsx (1)

35-43: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Swallowing all errors from image picker; no explicit permission request.

The catch {} hides every failure, not just user cancellation — this can mask real configuration problems (e.g. missing photo-library usage description) with no diagnostic trail. Official Expo docs also recommend calling requestMediaLibraryPermissionsAsync() before launchImageLibraryAsync to avoid an unexpected permission dialog appearing mid-flow.

♻️ Suggested improvement
 async function pickPhoto(onChange: (uri: string) => void) {
   try {
+    const permission = await ImagePicker.requestMediaLibraryPermissionsAsync();
+    if (!permission.granted) return;
     const result = await ImagePicker.launchImageLibraryAsync({
       mediaTypes: ["images"],
       quality: 1,
     });
     if (!result.canceled) onChange(result.assets[0].uri);
-  } catch {}
+  } catch (error) {
+    console.warn("Не удалось выбрать фото", error);
+  }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/shared/ui/PhotoPicker.tsx` around lines 35 - 43, The `pickPhoto` helper
in `PhotoPicker.tsx` is swallowing all image picker failures and never requests
media library permission up front. Update `pickPhoto` to call
`ImagePicker.requestMediaLibraryPermissionsAsync()` before
`launchImageLibraryAsync`, handle denied permission explicitly, and replace the
empty `catch` with error logging or propagation so real configuration issues are
visible while still ignoring user cancellation in
`ImagePicker.launchImageLibraryAsync`.
package.json (1)

13-13: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Сверьте версию expo-image-picker с рекомендованной для SDK 56.

В документации SDK 56 для expo-image-picker сейчас указана рекомендованная версия ~56.0.20, а здесь зафиксирована ~56.0.18. Лучше выровнять это до рекомендованного патча или хотя бы отдельно проверить, что проект не начнёт ругаться на несовместимость модуля. (docs.expo.dev)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@package.json` at line 13, The expo-image-picker dependency is pinned to an
older SDK 56 patch than the recommended version. Update the version in
package.json from the current expo-image-picker entry to the SDK 56 recommended
patch and verify the app still installs and runs cleanly with Expo SDK 56 so
there are no module compatibility warnings.
app/s/[slug].tsx (1)

158-158: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use shared date-formatting helper instead of raw toLocaleString().

Other parts of the stack introduced shared datetime helpers (src/shared/lib/datetime.ts). Using toLocaleString() directly here risks inconsistent date/time formatting compared to the rest of the app.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/s/`[slug].tsx at line 158, The meeting start time in the slug page is
formatted with raw Date.toLocaleString(), which can drift from the app’s shared
formatting. Update the render in the [slug] page to use the datetime helper from
src/shared/lib/datetime instead of formatting inline. Locate the Text element
that displays meeting.starts_at and replace the direct locale formatting with
the shared helper so this screen matches the rest of the app’s date/time
presentation.
src/entities/series/mappers.ts (1)

31-38: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing test coverage for mapUpdateSeriesFormToInput.

mappers.test.ts covers mapCreateSeriesFormToInput, mapJoinFormToMemberInput, and mapSeriesToUpdateForm, but not this function, despite it performing the same date-conversion logic that's tested elsewhere and being on the critical path for the manage-series save flow.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/entities/series/mappers.ts` around lines 31 - 38, Add test coverage for
mapUpdateSeriesFormToInput in mappers.test.ts. Create a case that verifies it
preserves the rest of the UpdateSeriesForm fields while converting starts_at
through toIsoDate, similar to the existing tests for mapCreateSeriesFormToInput
and mapSeriesToUpdateForm. Keep the test focused on the manage-series save flow
path by asserting the returned UpdateSeriesInput shape from
mapUpdateSeriesFormToInput.
src/shared/ui/ChatLinkButton.tsx (1)

8-14: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Silent no-op if the URL can't be opened.

No canOpenURL check or user-facing feedback when openURL fails (e.g., unsupported scheme, no handling app). Consider surfacing a fallback message so the tap isn't a dead click.

Also verify chat_url values (including those produced by telegramToChatUrl) are always https:// links rather than custom URI schemes, since custom schemes may require additional iOS LSApplicationQueriesSchemes configuration to open reliably.

💡 Proposed fix: check openability and surface failure
 export function ChatLinkButton({ url }: Props) {
   function openChat() {
-    Linking.openURL(url).catch(() => {});
+    Linking.canOpenURL(url)
+      .then((supported) => {
+        if (supported) return Linking.openURL(url);
+        // TODO: показать пользователю сообщение о недоступной ссылке
+      })
+      .catch(() => {});
   }
 
   return <Button title="Перейти в чат" onPress={openChat} />;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/shared/ui/ChatLinkButton.tsx` around lines 8 - 14, The ChatLinkButton
openChat flow silently ignores Linking.openURL failures, so taps can become dead
clicks. Update ChatLinkButton to check whether the URL can be opened before
calling openURL, and surface a user-facing fallback message if it cannot; keep
the logic centered around openChat and Linking. Also verify that chat_url
values, including those from telegramToChatUrl, are always https:// links rather
than custom URI schemes so they open reliably without extra platform
configuration.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app.json`:
- Around line 25-33: The expo-image-picker plugin config in app.json is missing
the explicit microphone opt-out, so Android may still request RECORD_AUDIO
unnecessarily. Update the expo-image-picker options alongside photosPermission
to set microphonePermission to false, keeping the change localized to the
expo-image-picker entry in the plugins array.

In `@app/index.tsx`:
- Around line 17-21: The main feed filters are only applied to useActivities, so
useSeries still loads all series and the combined feed ignores city/query.
Update app/index.tsx so the same trimmed filter values are passed into useSeries
(or otherwise applied before buildFeed), keeping the activities and series data
in sync with the current filter state.
- Around line 108-115: The nextMeetingDate() helper currently falls back to
series.starts_at, which can return a past date and incorrectly mark a series as
“ближайшая” when no future meetings exist. Update nextMeetingDate() to compute
the next occurrence from the series recurrence rule instead of using the
original starts_at fallback, and keep the existing upcoming meetings logic for
active future slots.

In `@src/entities/activity/local.ts`:
- Around line 21-26: `addMyActivity` and `toggleLike` currently do a
read-modify-write against the same device storage keys without coordination, so
parallel calls can overwrite each other’s updates. Introduce a per-key
serialized/atomic update helper or queue around the
`readMyActivities`/`writeDeviceJson` flow in `src/entities/activity/local.ts`,
and route both `addMyActivity` and `toggleLike` through it so only one mutation
for a given key runs at a time.

In `@src/entities/profile/schemas.ts`:
- Around line 3-4: `profile/schemas.ts` is re-declaring the `optionalText`
validator instead of using the shared one, which can drift from the common
empty-string-to-null behavior. Remove the local `emptyToNull`/`optionalText`
setup and import the shared `optionalText` from `src/shared/lib/zodFields.ts`,
then update the schema definitions in `schemas.ts` to use that shared validator
everywhere this field pattern appears.

In `@src/entities/series/schemas.ts`:
- Around line 19-26: The recurrence schema in `series/schemas.ts` allows
unbounded expansion because `count` only checks positivity and `until` only
checks date validity. Tighten the validators in the recurrence union so `count`
has a reasonable upper limit and `until` is capped to a maximum horizon relative
to `starts_at` (or another fixed max window). Make the same bound enforcement
visible to the occurrence generation path in `ensureUpcomingMeetings` in
`api.ts` so oversized recurrences cannot materialize excessive `meetings` rows.

In `@src/entities/series/utils.test.ts`:
- Line 20: В тестах `series/utils.test.ts` строковые названия в `describe(...)`
сейчас на английском, хотя для `.ts`-файлов весь контент должен быть на русском.
Переведите названия всех затронутых `describe(...)` в этом файле на русский,
сохранив смысл и структуру групп тестов, чтобы вывод `series/utils
listOccurrences` и связанных блоков был полностью локализован.

In `@src/features/create-series/RecurrenceFields.tsx`:
- Around line 174-181: The recurrence preview in toRecurrence uses a loose
Number(draft.interval) || 1 fallback, which incorrectly converts 0 to 1 and
still lets negative values through. Update toRecurrence in RecurrenceFields so
interval is normalized with explicit validation: treat non-numeric, zero, and
negative inputs as the default positive value (or clamp to a minimum of 1)
before listOccurrences uses it, while keeping the rest of the recurrence shape
unchanged.
- Around line 128-140: The count input in RecurrenceFields is storing
unvalidated Number(count) values, which can become NaN and then render as the
literal “NaN” through value={String(value.count)}. Update the onChangeText
handler for the count field to validate/parsing-safe the input before calling
onChange, and avoid committing NaN (for example by treating invalid/empty input
as a non-update or a safe fallback). Apply the same guard in the other affected
count input block referenced by the comment so both uses of the count field stay
consistent.

In `@src/features/join-series/useJoinSeries.ts`:
- Around line 35-50: The join flow in join() treats addMembership failures the
same as remote join failures, so a local-storage error after
joinMutation.mutateAsync succeeds is misreported. Split the try/catch in
useJoinSeries so the remote mutation is handled separately from addMembership
and setMembership; if addMembership throws, surface a storage-specific error
without implying the series join failed. Keep the existing success path around
joinMutation.mutateAsync, mapJoinFormToMemberInput, and setFormServerError, and
preserve the remote member.id/series.slug state only after the server join
completes.
- Around line 52-58: The leave() flow in useJoinSeries should handle failures
the same way join() does instead of allowing unhandled rejections. Wrap
leaveMutation.mutateAsync and removeMembership in a try/catch inside leave(),
surface the error through the same error-handling path used by join(), and only
clear membership with setMembership(null) after both operations succeed.

In `@src/features/like-activity/useLikeActivity.ts`:
- Around line 25-32: The toggle() flow in useLikeActivity currently flips
isLiked optimistically but never restores the previous value when
toggleLike(slug) fails, and it allows overlapping calls from rapid taps. Update
toggle() to keep the prior liked state, rollback the optimistic setIsLiked
change inside the catch path on persistence failure, and add a simple in-flight
guard around the toggleLike call so a second toggle() invocation is ignored
until the first completes.

In `@src/features/manage-series/ManageSeriesForm.tsx`:
- Around line 108-113: The Save/Finish actions in ManageSeriesForm are using
isSaving only for the label, so repeated taps can trigger duplicate
updateMutation.mutateAsync calls. Update the Button props in ManageSeriesForm so
both the “Сохранить” and “Завершить серию” actions are disabled while isSaving
is true, following the same pattern used by JoinSeriesForm’s
disabled={isJoining}. Keep the existing handleSubmit(onSubmit) and onFinish
handlers, but gate both buttons with the shared saving state.

In `@src/features/manage-series/useManageSeriesForm.ts`:
- Around line 66-72: The reschedule/cancel helpers in useManageSeriesForm
currently call mutateAsync without handling failures and do not expose loading
state. Update rescheduleMeeting and cancelMeeting to mirror submit/finishSeries
by catching mutation errors and surfacing them through the form’s existing error
handling flow, and expose the corresponding pending flags from
rescheduleMutation and cancelMutation so MeetingRow can disable the “Перенести”
and “Отменить встречу” actions while a request is in flight.

In `@src/shared/ui/MultiSelect.tsx`:
- Around line 6-8: The Option label type in MultiSelect is too broad for how it
is used, since the value is later coerced to a string in MultiSelect and can
turn JSX into an invalid “[object Object]” label. Tighten the Option definition
and the MultiSelect rendering logic so option.label is either a string-only
field or is rendered as a ReactNode without string interpolation; update the
code paths around Option and MultiSelect to keep text labels separate from any
node-based display.

---

Nitpick comments:
In `@app/s/`[slug].tsx:
- Line 158: The meeting start time in the slug page is formatted with raw
Date.toLocaleString(), which can drift from the app’s shared formatting. Update
the render in the [slug] page to use the datetime helper from
src/shared/lib/datetime instead of formatting inline. Locate the Text element
that displays meeting.starts_at and replace the direct locale formatting with
the shared helper so this screen matches the rest of the app’s date/time
presentation.

In `@package.json`:
- Line 13: The expo-image-picker dependency is pinned to an older SDK 56 patch
than the recommended version. Update the version in package.json from the
current expo-image-picker entry to the SDK 56 recommended patch and verify the
app still installs and runs cleanly with Expo SDK 56 so there are no module
compatibility warnings.

In `@src/entities/series/mappers.ts`:
- Around line 31-38: Add test coverage for mapUpdateSeriesFormToInput in
mappers.test.ts. Create a case that verifies it preserves the rest of the
UpdateSeriesForm fields while converting starts_at through toIsoDate, similar to
the existing tests for mapCreateSeriesFormToInput and mapSeriesToUpdateForm.
Keep the test focused on the manage-series save flow path by asserting the
returned UpdateSeriesInput shape from mapUpdateSeriesFormToInput.

In `@src/features/create-activity/useCreateActivityForm.ts`:
- Around line 63-67: The rememberCreated helper in useCreateActivityForm
currently swallows track.mutateAsync failures with an empty catch, making
tracking issues invisible. Keep the non-blocking behavior, but add lightweight
logging in the catch block (using the existing logger/error-reporting pattern in
this feature or nearby hooks) so failures are observable without affecting the
create flow.

In `@src/features/create-series/CreateSeriesForm.tsx`:
- Around line 17-68: The form field block in CreateSeriesForm is duplicated from
the shared activity form, so it should be extracted into a reusable core fields
component or helper used by both CreateSeriesForm and CreateActivityForm. Move
the shared FormField wiring for title, description, city, location_text,
capacity, cover_url, and chat_url into a common component, and keep only the
series-specific starts_at label/behavior in CreateSeriesForm so future
validation or label changes stay in sync.

In `@src/features/create-series/useCreateSeriesForm.ts`:
- Line 4: The series form is importing telegramToChatUrl from an
activity-specific utils module, creating unnecessary cross-domain coupling
between series and activity. Move telegramToChatUrl out of
entities/activity/utils into a shared/profile-owned module, then update both
useCreateActivityForm and useCreateSeriesForm to import it from the new
location. Keep the helper’s behavior unchanged and ensure the series feature no
longer depends on activity-only utilities.

In `@src/shared/lib/publicLink.ts`:
- Around line 24-26: The slug suffix generation in shortRandom currently uses
Math.random(), which is acceptable for deduplication but inconsistent with the
crypto-based approach used by generateEditToken. Update shortRandom in
publicLink.ts to use the same expo-crypto random source for generating the
6-character suffix, keeping the helper’s behavior the same while aligning it
with the rest of the token generation logic.

In `@src/shared/lib/shareActivity.test.ts`:
- Around line 1-45: Add test coverage for the web fallback in shareActivity
tests, since only the native Platform.OS: "ios" path is covered now. Create a
separate describe block that mocks Platform.OS as "web" and exercises the
shouldCopyInstead/copyLink flow, including cases where navigator.clipboard is
missing and where clipboard write rejects. Keep the assertions focused on the
shareActivity result shape and the fallback behavior.

In `@src/shared/lib/storage.ts`:
- Around line 16-24: `readDeviceJson` currently trusts `JSON.parse(raw)` and
returns it as `T` without checking shape, so stale or corrupted storage can flow
through as if valid. Update `readDeviceJson` in `storage.ts` to accept an
optional Zod schema/validator (consistent with `zodFields.ts`), parse the JSON,
and run `.safeParse` before returning. If parsing fails or the value does not
match the schema, return `null`; keep the existing `null` behavior for missing
or invalid JSON.

In `@src/shared/lib/zodFields.ts`:
- Around line 12-41: The url validation in optionalUrl and chatUrl still uses
the deprecated z.string().url() chain, so update both schemas to use the
top-level z.url() API instead. Keep the existing emptyToNull preprocessing and
nullable behavior, and preserve the current error message and isKnownChatHost
refinement in chatUrl so the validation logic remains unchanged.

In `@src/shared/ui/ChatLinkButton.tsx`:
- Around line 8-14: The ChatLinkButton openChat flow silently ignores
Linking.openURL failures, so taps can become dead clicks. Update ChatLinkButton
to check whether the URL can be opened before calling openURL, and surface a
user-facing fallback message if it cannot; keep the logic centered around
openChat and Linking. Also verify that chat_url values, including those from
telegramToChatUrl, are always https:// links rather than custom URI schemes so
they open reliably without extra platform configuration.

In `@src/shared/ui/PhotoPicker.tsx`:
- Around line 35-43: The `pickPhoto` helper in `PhotoPicker.tsx` is swallowing
all image picker failures and never requests media library permission up front.
Update `pickPhoto` to call `ImagePicker.requestMediaLibraryPermissionsAsync()`
before `launchImageLibraryAsync`, handle denied permission explicitly, and
replace the empty `catch` with error logging or propagation so real
configuration issues are visible while still ignoring user cancellation in
`ImagePicker.launchImageLibraryAsync`.

In `@src/shared/ui/ShareButton.tsx`:
- Around line 13-19: The ShareButton share() handler can be invoked multiple
times before the previous request completes, causing duplicate share actions.
Update ShareButton to guard against re-entry in share(), using the existing
share() function and its state so rapid taps are ignored until shareActivity({
slug, title }) resolves, then clear the guard after setNotice(noticeFor(result))
runs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b47d749b-14f6-4181-adc5-07485ec1b573

📥 Commits

Reviewing files that changed from the base of the PR and between 5d4b49a and c68e8a5.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (86)
  • TESTS.md
  • app.json
  • app/_layout.tsx
  • app/a/[slug].tsx
  • app/create.tsx
  • app/index.tsx
  • app/manage/[editToken].tsx
  • app/manage/series/[editToken].tsx
  • app/profile/edit.tsx
  • app/profile/index.tsx
  • app/s/[slug].tsx
  • docs/about_the_project.md
  • docs/todo.md
  • package.json
  • src/entities/activity/api.ts
  • src/entities/activity/hooks.ts
  • src/entities/activity/local.test.ts
  • src/entities/activity/local.ts
  • src/entities/activity/mappers.test.ts
  • src/entities/activity/mappers.ts
  • src/entities/activity/schemas.test.ts
  • src/entities/activity/schemas.ts
  • src/entities/activity/types.ts
  • src/entities/activity/utils.test.ts
  • src/entities/activity/utils.ts
  • src/entities/participant/hooks.ts
  • src/entities/participant/local.test.ts
  • src/entities/participant/local.ts
  • src/entities/participant/mappers.test.ts
  • src/entities/participant/mappers.ts
  • src/entities/participant/schemas.test.ts
  • src/entities/participant/schemas.ts
  • src/entities/participant/types.ts
  • src/entities/participant/utils.ts
  • src/entities/profile/hooks.ts
  • src/entities/profile/mappers.test.ts
  • src/entities/profile/mappers.ts
  • src/entities/profile/schemas.test.ts
  • src/entities/profile/schemas.ts
  • src/entities/profile/storage.ts
  • src/entities/profile/types.ts
  • src/entities/profile/utils.test.ts
  • src/entities/profile/utils.ts
  • src/entities/series/api.ts
  • src/entities/series/hooks.ts
  • src/entities/series/local.test.ts
  • src/entities/series/local.ts
  • src/entities/series/mappers.test.ts
  • src/entities/series/mappers.ts
  • src/entities/series/schemas.test.ts
  • src/entities/series/schemas.ts
  • src/entities/series/types.ts
  • src/entities/series/utils.test.ts
  • src/entities/series/utils.ts
  • src/features/create-activity/CreateActivityForm.tsx
  • src/features/create-activity/useCreateActivityForm.ts
  • src/features/create-series/CreateSeriesForm.tsx
  • src/features/create-series/RecurrenceFields.tsx
  • src/features/create-series/useCreateSeriesForm.ts
  • src/features/edit-profile/EditProfileForm.tsx
  • src/features/edit-profile/useEditProfileForm.ts
  • src/features/join-series/JoinSeriesForm.tsx
  • src/features/join-series/useJoinSeries.ts
  • src/features/like-activity/useLikeActivity.ts
  • src/features/manage-activity/ManageActivityForm.tsx
  • src/features/manage-activity/useManageActivityForm.ts
  • src/features/manage-series/ManageSeriesForm.tsx
  • src/features/manage-series/useManageSeriesForm.ts
  • src/features/mark-meeting/useMarkMeeting.ts
  • src/features/respond-to-activity/ResponseForm.tsx
  • src/features/respond-to-activity/useRespondToActivityForm.ts
  • src/shared/lib/datetime.test.ts
  • src/shared/lib/datetime.ts
  • src/shared/lib/publicLink.test.ts
  • src/shared/lib/publicLink.ts
  • src/shared/lib/publicUrl.ts
  • src/shared/lib/shareActivity.test.ts
  • src/shared/lib/shareActivity.ts
  • src/shared/lib/storage.ts
  • src/shared/lib/zodFields.ts
  • src/shared/ui/Avatar.tsx
  • src/shared/ui/ChatLinkButton.tsx
  • src/shared/ui/MultiSelect.tsx
  • src/shared/ui/ParticipantList.tsx
  • src/shared/ui/PhotoPicker.tsx
  • src/shared/ui/ShareButton.tsx
💤 Files with no reviewable changes (3)
  • src/entities/participant/types.ts
  • src/entities/participant/mappers.ts
  • src/entities/participant/utils.ts

Comment thread app.json
Comment thread app/index.tsx
Comment thread app/index.tsx Outdated
Comment thread src/entities/activity/local.ts Outdated
Comment thread src/entities/profile/schemas.ts Outdated
Comment thread src/features/join-series/useJoinSeries.ts
Comment thread src/features/like-activity/useLikeActivity.ts
Comment thread src/features/manage-series/ManageSeriesForm.tsx
Comment thread src/features/manage-series/useManageSeriesForm.ts
Comment thread src/shared/ui/MultiSelect.tsx Outdated
- главная лента фильтрует серии по городу/тексту (index, series/api,hooks)
- ближайшая встреча серии считается по правилу, прошедший старт не всплывает
- запись/выход из серии обрабатывают ошибки, сбой хранилища не ломает join
- ограничение числа встреч в схеме серии
- защита от NaN и неположительного интервала в конструкторе повторения
- дизейбл кнопок сохранения/переноса/отмены и лайка на время запроса
- откат оптимистичного лайка при сбое записи + guard от двойного тапа
- сериализация read-modify-write device-хранилища (updateDeviceJson)
- MultiSelect.label сужен до string, дубль optionalText убран в shared
- microphonePermission:false у expo-image-picker, русские describe в тестах
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant