From 6cc905b5d006ce0933720c10da397c4e92edae17 Mon Sep 17 00:00:00 2001 From: Ivan Lukov Date: Sat, 30 May 2026 14:06:46 +0500 Subject: [PATCH 01/15] =?UTF-8?q?=D0=A3=D1=80=D0=BE=D0=B4=D0=BB=D0=B8?= =?UTF-8?q?=D0=B2=D0=B0=D1=8F=20=D0=BF=D0=B5=D1=80=D0=B2=D0=B0=D1=8F=20?= =?UTF-8?q?=D0=B2=D0=B5=D1=80=D1=81=D0=B8=D1=8F=20=D0=BF=D1=80=D0=BE=D0=B5?= =?UTF-8?q?=D0=BA=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 2 + .github/workflows/quality.yml | 26 ++++ .husky/pre-commit | 1 + .husky/pre-push | 1 + AGENTS.md | 8 +- README.md | 22 ++-- app.json | 5 +- app/_layout.tsx | 44 +++---- app/a/[slug].tsx | 176 +++++++++++++++++++++---- app/create.tsx | 107 ++++++++++++--- app/index.tsx | 124 ++++-------------- app/manage/[editToken].tsx | 193 ++++++++++++++++++++++++++++ babel.config.js | 2 +- biome.jsonc | 54 ++++++++ nativewind-env.d.ts | 2 +- package-lock.json | 181 ++++++++++++++++++++++++++ package.json | 8 +- src/entities/activity/api.ts | 58 +++++++++ src/entities/activity/hooks.ts | 48 +++++++ src/entities/activity/schemas.ts | 26 ++++ src/entities/activity/types.ts | 34 +++++ src/entities/activity/utils.ts | 26 ++++ src/entities/participant/api.ts | 22 ++++ src/entities/participant/hooks.ts | 23 ++++ src/entities/participant/schemas.ts | 10 ++ src/entities/participant/types.ts | 20 +++ src/shared/supabase/client.ts | 28 ++++ src/shared/ui/Button.tsx | 18 +++ src/shared/ui/ErrorText.tsx | 8 ++ src/shared/ui/Input.tsx | 19 +++ src/shared/ui/LoadingText.tsx | 4 + src/shared/ui/Page.tsx | 10 ++ src/shared/ui/Select.tsx | 40 ++++++ src/shared/ui/Textarea.tsx | 7 + src/utils/supabase.ts | 38 +++--- 35 files changed, 1192 insertions(+), 203 deletions(-) create mode 100644 .github/workflows/quality.yml create mode 100755 .husky/pre-commit create mode 100755 .husky/pre-push create mode 100644 app/manage/[editToken].tsx create mode 100644 biome.jsonc create mode 100644 src/entities/activity/api.ts create mode 100644 src/entities/activity/hooks.ts create mode 100644 src/entities/activity/schemas.ts create mode 100644 src/entities/activity/types.ts create mode 100644 src/entities/activity/utils.ts create mode 100644 src/entities/participant/api.ts create mode 100644 src/entities/participant/hooks.ts create mode 100644 src/entities/participant/schemas.ts create mode 100644 src/entities/participant/types.ts create mode 100644 src/shared/supabase/client.ts create mode 100644 src/shared/ui/Button.tsx create mode 100644 src/shared/ui/ErrorText.tsx create mode 100644 src/shared/ui/Input.tsx create mode 100644 src/shared/ui/LoadingText.tsx create mode 100644 src/shared/ui/Page.tsx create mode 100644 src/shared/ui/Select.tsx create mode 100644 src/shared/ui/Textarea.tsx diff --git a/.env.example b/.env.example index c900474..579390c 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,4 @@ EXPO_PUBLIC_SUPABASE_URL= EXPO_PUBLIC_SUPABASE_KEY= + +COPY INTO .env.local diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml new file mode 100644 index 0000000..a74359d --- /dev/null +++ b/.github/workflows/quality.yml @@ -0,0 +1,26 @@ +name: quality + +on: + pull_request: + push: + branches: + - main + +jobs: + quality: + runs-on: ubuntu-latest + steps: + - name: Забрать код + uses: actions/checkout@v4 + + - name: Настроить Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + + - name: Установить зависимости + run: npm ci + + - name: Проверить Biome и TypeScript + run: npm run ci diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..5d6c57c --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +npx biome check --write --staged --files-ignore-unknown=true diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100755 index 0000000..320512b --- /dev/null +++ b/.husky/pre-push @@ -0,0 +1 @@ +npm run ci diff --git a/AGENTS.md b/AGENTS.md index a26b4bb..435416e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,3 +1,7 @@ -# Expo HAS CHANGED +# Expo ИЗМЕНИЛСЯ -Read the exact versioned docs at https://docs.expo.dev/versions/v56.0.0/ before writing any code. +Перед написанием кода читай точную документацию по версии: https://docs.expo.dev/versions/v56.0.0/ + +# Язык проекта + +Весь контент, комментарии и документация должны быть на русском языке. diff --git a/README.md b/README.md index 5b978a8..43d66ab 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,14 @@ -# project-e -Лента локальных событий происходящих вокруг тебя +# Венти + +Лента локальных событий вокруг тебя. # Стек -- React Native - web, ios, android. Единый UI фреймворк во избежании поддержки разных версий компонентов -- Supabase - BaaS чтобы не делать бэкенд, со временем добавим бэкенд сервис который будет общаться с supabase или постгресом напрямую, но пока сэкономит нам время -- Nativewind - настроил :) -- Zustand - UI стейт -- React Query - server стейт -- React Hook Form / Zod - -Мб понадобится storybook + +- React Native — веб, iOS и Android. Единый UI-фреймворк, чтобы не поддерживать разные версии компонентов. +- Supabase — сервис базы данных, чтобы пока не писать отдельный бэкенд. +- NativeWind — стили через классы. +- Zustand — состояние интерфейса, если понадобится. +- React Query — серверное состояние. +- React Hook Form / Zod — формы и валидация. + +Возможно, позже понадобится Storybook. diff --git a/app.json b/app.json index fae87e4..f306c05 100644 --- a/app.json +++ b/app.json @@ -21,9 +21,6 @@ "web": { "favicon": "./assets/favicon.png" }, - "plugins": [ - "expo-router", - "expo-status-bar" - ] + "plugins": ["expo-router", "expo-status-bar"] } } diff --git a/app/_layout.tsx b/app/_layout.tsx index a229637..60a2808 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -1,45 +1,33 @@ import "../global.css"; - +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { Link, Stack } from "expo-router"; +import { useState } from "react"; import { Text, View } from "react-native"; -export const SITE_PAGES = [ - { href: "/", label: "Home" }, - { href: "/create", label: "Create" }, - { href: "/a/welcome", label: "Welcome article" }, - { href: "/a/supabase", label: "Supabase article" }, -] as const; - -const linkClassName = "text-base text-blue-600 underline"; - -export function PageLinks() { - return ( - - {SITE_PAGES.map((page) => ( - - {page.label} - - ))} - - ); -} - export function BackLink() { return ( - ← Back to all pages + ← На главную ); } export default function RootLayout() { + const [client] = useState(() => new QueryClient()); return ( - <> - - Venty - + + + Венти + + + Главная + + + Создать + + - + ); } diff --git a/app/a/[slug].tsx b/app/a/[slug].tsx index 307d569..954a3c9 100644 --- a/app/a/[slug].tsx +++ b/app/a/[slug].tsx @@ -1,30 +1,162 @@ -import { Link, useLocalSearchParams } from "expo-router"; -import { ScrollView, Text, View } from "react-native"; -import { BackLink, SITE_PAGES } from "../_layout"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useLocalSearchParams } from "expo-router"; +import type { ComponentType } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { Image, Text, View } from "react-native"; +import { useActivityBySlug } from "../../src/entities/activity/hooks"; +import { + useCreateParticipant, + useParticipants, +} from "../../src/entities/participant/hooks"; +import { + type ParticipantForm, + participantSchema, +} from "../../src/entities/participant/schemas"; +import { Button } from "../../src/shared/ui/Button"; +import { ErrorText } from "../../src/shared/ui/ErrorText"; +import { Input, type InputProps } from "../../src/shared/ui/Input"; +import { LoadingText } from "../../src/shared/ui/LoadingText"; +import { Page } from "../../src/shared/ui/Page"; +import { Select } from "../../src/shared/ui/Select"; +import { Textarea } from "../../src/shared/ui/Textarea"; +import { BackLink } from "../_layout"; -export default function ArticlePage() { +function statusText(status: string) { + if (status === "going") return "иду"; + if (status === "maybe") return "может быть"; + if (status === "cant") return "не могу"; + return status; +} + +export default function ActivityPage() { const { slug } = useLocalSearchParams<{ slug: string }>(); - const title = slug ? slug.replaceAll("-", " ") : "article"; + const aq = useActivityBySlug(slug); + const activity = aq.data; + const pq = useParticipants(activity?.id); + const m = useCreateParticipant(activity?.id ?? ""); + const { + control, + handleSubmit, + reset, + formState: { errors }, + } = useForm({ + resolver: zodResolver(participantSchema), + defaultValues: { name: "", telegram: "", status: "going", comment: "" }, + }); + if (aq.isLoading) + return ( + + + + ); + if (!activity) + return ( + + + Активность не найдена + + {aq.error instanceof Error ? aq.error.message : null} + + + ); + const ps = pq.data ?? []; + const count = (s: string) => ps.filter((p) => p.status === s).length; + async function submit(v: ParticipantForm) { + const currentActivity = activity; + if (!currentActivity) return; + await m.mutateAsync({ + activity_id: currentActivity.id, + ...v, + telegram: v.telegram || null, + comment: v.comment || null, + }); + reset({ name: "", telegram: "", status: "going", comment: "" }); + } + const field = ( + name: keyof ParticipantForm, + label: string, + C: ComponentType = Input, + ) => ( + ( + + )} + /> + ); return ( - - - - {title} - - Dynamic route: /a/{slug}. This page proves nested pages render on web and have a backlink - to the all-pages index. + + + {activity.cover_url ? ( + + ) : null} + {activity.title} + {activity.city} + {activity.location_text} + {new Date(activity.starts_at).toLocaleString()} + {activity.description} + {activity.status === "cancelled" ? ( + ОТМЕНЕНО + ) : null} + + Идут: {count("going")} Может быть: {count("maybe")} Не могут:{" "} + {count("cant")} + + {activity.capacity ? ( + + {count("going")} / {activity.capacity} идут - - - Other pages - {SITE_PAGES.filter((page) => page.href !== `/a/${slug}`).map((page) => ( - - {page.label} - - ))} + ) : null} + {activity.status !== "cancelled" ? ( + + Форма отклика + {field("name", "имя, обязательно")} + {field("telegram", "телеграм")} + { + ( + + )} + /> +