Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"react-native-safe-area-context": "5.4.0",
"react-native-screens": "~4.11.1",
"react-native-svg": "15.11.2",
"react-native-webview": "13.13.5",
"socket.io-client": "2.3.1",
"tailwind-merge": "^3.3.0",
"tailwindcss": "3",
Expand Down
50 changes: 5 additions & 45 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,17 @@ import { useAuthStore } from "./shared/stores/auth.store";
import { useGeolocationStore } from "./shared/stores/location.store";
import { logger } from "./shared/instances/logger.instance";
import React from "react";
import { View, Text } from "react-native";
import { ModalProvider } from "./shared/providers/modal-provider";
import { OverlayProvider } from "./shared/providers/overlay.provider";
import { SocketErrorModalBridge } from "./shared/instances/socket.instance";
import { ErrorBoundary } from "./shared/error-boundary";

SplashScreen.preventAutoHideAsync();

ErrorUtils.setGlobalHandler((error, isFatal) => {
logger.error("ui", "Global error:", error, isFatal);
});

const queryClient = new QueryClient();

export default function App() {
Expand Down Expand Up @@ -73,50 +77,6 @@ export default function App() {
return null;
}

class ErrorBoundary extends React.Component<
{ children: React.ReactNode },
{ hasError: boolean }
> {
constructor(props: any) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(err: any) {
console.error("Unhandled UI error:", err);
}
render() {
if (this.state.hasError) {
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<SafeAreaProvider>
<NavigationContainer>
<View
style={{
flex: 1,
alignItems: "center",
justifyContent: "center",
padding: 24,
}}
>
<Text
style={{ color: "#fff", fontSize: 18, textAlign: "center" }}
>
Произошла ошибка. Приложение продолжает работу. Вернитесь
назад.
</Text>
</View>
</NavigationContainer>
</SafeAreaProvider>
</GestureHandlerRootView>
);
}
return this.props.children as any;
}
}

return (
<QueryClientProvider client={queryClient}>
<ErrorBoundary>
Expand Down
8 changes: 4 additions & 4 deletions client/src/components/overlays/task-point.overlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,10 @@ export const TaskPointOverlay = ({
{/* Content Sleeve */}
<View className="flex justify-between px-8 pt-10 bg-[#FFF] w-full h-[65%] rounded-t-[40px] z-10">
<View className="gap-5">
<Text className="text-3xl font-bounded-medium">
<Text className="text-3xl font-bounded-medium text-black">
{taskPoint.title}
</Text>
<Text className="text-xl">{taskPoint.description}</Text>
<Text className="text-xl text-black">{taskPoint.description}</Text>
<View className="bg-[#9090901A] w-full p-3 gap-4 rounded-2xl flex flex-row">
<View className="bg-[#FFF] h-[72px] w-[72px] rounded-xl overflow-hidden flex justify-center items-center">
{completedTaskPoint ? (
Expand All @@ -104,13 +104,13 @@ export const TaskPointOverlay = ({
<View className="flex-1">
{/* Takes remaining space */}
<Text
className="text-lg font-bounded-medium"
className="text-lg font-bounded-medium text-black"
numberOfLines={1} // Optional: limit title lines
>
{taskPoint.title}
</Text>
<Text
className="text-md"
className="text-md text-black"
numberOfLines={3} // Optional: limit description lines
>
{taskPoint.task}
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/screens/main/game/route.screen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ export const RouteScreen = () => {
// Show only free/accessible routes for now (no ownership info available on client)
const routes = useMemo(() => {
const list = isFetched && data !== undefined ? data : [];
return list.filter((r) => r.priceRoubles === 0);
return list.filter((r) => r.bought === true || r.priceRoubles === 0);
}, [isFetched, data]);

const [index, setIndex] = useState(0);
Expand Down
140 changes: 89 additions & 51 deletions client/src/components/screens/main/shop.screen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import { RoutesWidget } from "@/components/widgets/shop/routes";
import { SafeAreaView } from "react-native-safe-area-context";
import { Header } from "@/components/ui/header";
import { StatusBar } from "expo-status-bar";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { useStyles } from "@/shared/api/hooks/useStyles";
import { useAuthStore } from "@/shared/stores/auth.store";
import { setAvatar } from "@/shared/api/styles.api";
Expand All @@ -21,24 +20,30 @@ import { Style } from "@/shared/interfaces/styles/styles";
import { useModal } from "@/shared/hooks/useModal";
import { logger } from "@/shared/instances/logger.instance";
import { useRoutes } from "@/shared/api/hooks/useRoutes";
import { WebView } from "react-native-webview";
import { buyRoute } from "@/shared/api/routes.api";
import { Route } from "@/shared/interfaces/route";

export const ShopScreen = () => {
const navigation = useNavigation<StackNavigationProp<MainStackParamList>>();
const sectionRef = useRef<{ selectedSection: string }>({
selectedSection: "Аватарки",
});

const [url, setUrl] = useState<null | string>(null);

const [selectedSeciton, setSelectedSection] = useState<string>("Аватарки");

const {
data: avatars,
isFetched: isAvatarsFetched,
buyStyle,
updateStyles,
} = useStyles({
type: "avatar",
});

const { data, isFetched } = useRoutes();
const { data, isFetched, updateRoutes } = useRoutes();
const routes = useMemo(
() => (isFetched && data !== undefined ? data : []),
[isFetched, data]
Expand All @@ -51,7 +56,7 @@ export const ShopScreen = () => {
navigation.goBack();
};

const handleButton = async (avatar: Style) => {
const handleAvatarButton = async (avatar: Style) => {
if (avatar.bought) {
try {
await setAvatar(avatar.id);
Expand Down Expand Up @@ -79,66 +84,99 @@ export const ShopScreen = () => {
});
}
} else {
await buyStyle(avatar.id);
var result = await buyStyle(avatar.id);
setUrl(result.confirmationUrl);
}
};

const handleRouteButton = async (r: Route) => {
var result = await buyRoute(r.id);
setUrl(result.confirmationUrl);
};

return (
<SafeAreaView className="flex-1 bg-bg_primary">
<ScrollView className="w-full">
<View className="w-[90%] gap-[36px] relative mx-auto flex-col items-center">
<View className="absolute w-full flex-row justify-between items-center">
<Pressable onPress={back}>
<IconContainer className="bg-[#222222]">
<Icons.Back />
</IconContainer>
</Pressable>
</View>
{url !== null ? (
<WebView
className="flex-1 w-full h-full"
source={{ uri: url }}
onNavigationStateChange={(state) => {
if (state.url.includes("success")) {
setUrl(null);

<Header
mainText={"Магазин"}
descriptionText={"Все, чтобы бежать стильно"}
/>
updateStyles();
updateRoutes();

<View className="flex items-center justify-center pb-10">
<SectionPicker
options={[...SHOP_SECTIONS]}
selectedRef={sectionRef}
onChange={setSelectedSection}
setModalOpen({
title: "Спасибо за покупку!",
subtitle:
"Покупка успешно завершена, можете применить аватар в магазине или найти маршрут в настройках лобби",
buttons: [
{
text: "Ок",
type: "primary",
onClick: () => setModalOpen(false),
},
],
});
}
}}
/>
) : (
<ScrollView className="w-full">
<View className="w-[90%] gap-[36px] relative mx-auto flex-col items-center">
<View className="absolute w-full flex-row justify-between items-center">
<Pressable onPress={back}>
<IconContainer className="bg-[#222222]">
<Icons.Back />
</IconContainer>
</Pressable>
</View>

<Header
mainText={"Магазин"}
descriptionText={"Все, чтобы бежать стильно"}
/>
</View>

{sectionRef.current?.selectedSection === "Аватарки" &&
isAvatarsFetched && (
<AvatarsWidget
cards={avatars!.map((avatar) => ({
avatar: avatar.style.url,
title: avatar.title,
withButton: true,
bought: avatar.bought,
buttonAction: async () => {
await handleButton(avatar);
},
subtitle:
avatar.priceRoubles == 0
? "Бесплатно"
: `${avatar.priceRoubles} ₽`,
disabled: avatar.id === user?.styles?.avatarId,
<View className="flex items-center justify-center pb-10">
<SectionPicker
options={[...SHOP_SECTIONS]}
selectedRef={sectionRef}
onChange={setSelectedSection}
/>
</View>

{sectionRef.current?.selectedSection === "Аватарки" &&
isAvatarsFetched && (
<AvatarsWidget
cards={avatars!.map((avatar) => ({
avatar: avatar.style.url,
title: avatar.title,
withButton: true,
bought: avatar.bought,
buttonAction: async () => await handleAvatarButton(avatar),
subtitle:
avatar.priceRoubles == 0
? "Бесплатно"
: `${avatar.priceRoubles} ₽`,
disabled: avatar.id === user?.styles?.avatarId,
}))}
className="pb-[70px]"
/>
)}

{sectionRef.current?.selectedSection === "Маршруты" && (
<RoutesWidget
routes={routes.filter(r => r.bought == false).map((r) => ({
route: r,
disabled: false,
buttonAction: async () => await handleRouteButton(r),
}))}
className="pb-[70px]"
/>
)}

{sectionRef.current?.selectedSection === "Маршруты" && (
<RoutesWidget
routes={routes.map((r) => ({
route: r,
disabled: false,
}))}
/>
)}
</View>
</ScrollView>
</View>
</ScrollView>
)}
<StatusBar style="light" />
</SafeAreaView>
);
Expand Down
14 changes: 11 additions & 3 deletions client/src/components/ui/button.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Pressable, Text } from "react-native";
import { Pressable, StyleProp, Text, ViewStyle } from "react-native";
import { cva, type VariantProps } from "class-variance-authority";
import { twMerge } from "tailwind-merge";
import React from "react";

const button = {
variant: {
Expand Down Expand Up @@ -39,12 +40,19 @@ interface ButtonProps extends VariantProps<typeof buttonVariants> {
className?: string;
onPress?: () => void;
text: string;
variant?: "default" | "disabled" | "invisible";
style?: StyleProp<ViewStyle>;
}

export const Button = ({ className, onPress, text, variant }: ButtonProps) => {
export const Button = ({
className,
onPress,
text,
variant,
style,
}: ButtonProps) => {
return (
<Pressable
style={style}
className={twMerge(buttonVariants({ variant }), className)}
onPress={variant == "disabled" ? undefined : onPress}
>
Expand Down
36 changes: 36 additions & 0 deletions client/src/components/ui/checkbox-input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Pressable, View, Text } from "react-native";
import { twMerge } from "tailwind-merge";
import { Icons } from './icons/icons';

interface CheckboxInputProps {
className?: string;
label?: string;
checked: boolean;
onChange: (checked: boolean) => void;
}

export const CheckboxInput = ({
className,
label,
checked,
onChange,
...props
}: CheckboxInputProps) => {
return (
<Pressable
className={twMerge("flex-row items-center", className)}
onPress={() => onChange(!checked)}
{...props}
>
<View
className={twMerge(
"h-6 w-6 rounded-lg border items-center justify-center",
checked ? "bg-[#8d57f2] border-[#8d57f2]" : "border-[#292B2D]"
)}
>
{checked && <Icons.Check />}
</View>
{label && <Text className="ml-3 text-lg text-text_primary">{label}</Text>}
</Pressable>
);
};
Loading