add simple home section

This commit is contained in:
2025-05-30 23:45:38 +02:00
parent eca62c8b45
commit 017b04116d
13 changed files with 480 additions and 19 deletions

View File

@@ -1,22 +1,24 @@
import { View, Text } from "react-native";
import { Link } from "expo-router";
import { Button, ButtonText } from "@/components/ui/button";
import { ScrollView, Text } from "react-native";
import { useNoticesStore } from '@/store/noticesStore';
import { CategorySection } from "@/components/CategorySection";
import { NoticeSection } from "@/components/NoticeSection";
import { UserSection } from "@/components/UserSection";
import { FlatList } from 'react-native';
export default function Home() {
const notices = useNoticesStore((state) => state.notices);
const latestNotices = [...notices]
.sort((a, b) => new Date(b.publishDate) - new Date(a.publishDate))
.slice(0, 6);
const recomendedNotices = [...notices]
.sort(() => Math.random() - 0.5)
.slice(0, 6);
return (
<View>
<Text>Home</Text>
<Link href="/notices" asChild>
<Button>
<ButtonText>Ogłoszenia</ButtonText>
</Button>
</Link>
<Link href="/wishlist" asChild>
<Button variant="outline" className="mt-2">
<ButtonText>Ulubione</ButtonText>
</Button>
</Link>
</View>
<ScrollView showsVerticalScrollIndicator={false} className='m-2'>
<CategorySection title="Polecane kategorie" notices={notices} />
<NoticeSection title="Najnowsze ogłoszenia" notices={latestNotices}/>
<UserSection title="Popularni sprzedawcy" notices={notices} />
<NoticeSection title="Proponowane ogłoszenia" notices={recomendedNotices}/>
</ScrollView>
);
}

View File

@@ -2,9 +2,16 @@ import { Stack } from "expo-router";
import "@/global.css";
import { GluestackUIProvider } from "@/components/ui/gluestack-ui-provider";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { useEffect } from "react";
import { useNoticesStore } from "@/store/noticesStore";
const queryClient = new QueryClient();
export default function RootLayout() {
const fetchNotices = useNoticesStore((state) => state.fetchNotices);
useEffect(() => {
fetchNotices();
}, []);
return (
<QueryClientProvider client={queryClient}>
<GluestackUIProvider>

View File

@@ -0,0 +1,52 @@
import { View} from 'react-native';
import { useEffect, useState } from 'react'
import { Heading } from '@/components/ui/heading';
import { Text } from '@/components/ui/text';
// import { useNoticesStore } from '@/store/noticesStore';
import { Pressable } from '@/components/ui/pressable';
import { FlatList } from 'react-native';
import axios from 'axios';
export function CategorySection({notices, title}) {
// const notices = useNoticesStore((state) => state.notices);
const [categoryMap, setCategoryMap] = useState({});
useEffect(() => {
axios.get('https://testowe.zikor.pl/api/v1/vars/categories')
.then(res => setCategoryMap(res.data))
.catch(() => setCategoryMap({}));
}, []);
const categories = Array.from(
new Set(notices.map((notice) => notice.category))
).filter(Boolean);
const getCount = (category) =>
notices.filter((notice) => notice.category === category).length;
return (
<View className="mb-6">
<Heading className="text-2xl font-bold mb-4 mt-4">{title}</Heading>
<FlatList
data={categories}
keyExtractor={(item) => item}
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={{ paddingHorizontal: 8, gap: 12 }}
renderItem={({ item }) => {
const categoryObj = categoryMap.find((cat) => cat.value === item);
return (
<Pressable className="bg-gray-200 p-4 rounded-lg mr-2">
<Text>
{categoryObj ? categoryObj.label : item} ({getCount(item)})
</Text>
</Pressable>
);
}}
/>
</View>
);
}

View File

@@ -0,0 +1,31 @@
import { View} from 'react-native';
import { Heading } from '@/components/ui/heading';
import { FlatList } from 'react-native';
import {NoticeCard} from "@/components/NoticeCard";
import { Box } from '@/components/ui/box';
import { HStack } from "@/components/ui/hstack"
import { VStack } from '@/components/ui/vstack';
import { Button, ButtonText } from "@/components/ui/button"
export function NoticeSection({ notices, title }) {
const rows = [];
for (let i = 0; i < notices.length; i += 2) {
rows.push(
<HStack key={i} space="md">
<NoticeCard notice={notices[i]} />
{notices[i + 1] && <NoticeCard notice={notices[i + 1]} />}
</HStack>
);
}
return (
<View className="mb-6">
<Heading className="text-2xl font-bold mb-4 mt-4">{title}</Heading>
<VStack space="md">
{rows}
</VStack>
<Button className="mt-6" size="md" variant="solid" action="primary">
<ButtonText>Zobacz więcej</ButtonText>
</Button>
</View>
);
}

View File

@@ -0,0 +1,23 @@
import { VStack } from '@/components/ui/vstack';
import { Avatar, AvatarImage, AvatarFallbackText } from "@/components/ui/avatar";
import { Heading } from "@/components/ui/heading";
import { Box } from '@/components/ui/box';
export default function UserBlock({ user }) {
return (
<Box className="rounded-md bg-white p-4 items-center justify-center mb-6" >
<VStack space="md" className='items-center'>
<Avatar>
<AvatarFallbackText>{user.firstName} {user.lastName}</AvatarFallbackText>
<AvatarImage
source={{
uri: user.image,
}}
/>
</Avatar>
<Heading size="sm">{user.firstName} {user.lastName}</Heading>
</VStack>
</Box>
);
}

View File

@@ -0,0 +1,44 @@
import { View} from 'react-native';
import { useEffect, useState } from 'react'
import { Heading } from '@/components/ui/heading';
import { FlatList } from 'react-native';
import axios from 'axios';
import UserBlock from '@/components/UserBlock';
export function UserSection({notices, title}) {
const [users, setUsers] = useState([]);
useEffect(() => {
axios.get('https://testowe.zikor.pl/api/v1/clients/get/all')
.then(res => setUsers(res.data))
.catch(() => setUsers([]));
}, []);
const usersWithNoticeCount = users.map(user => {
const count = notices.filter(n => n.clientId === user.id).length;
return { ...user, noticeCount: count };
});
const topUsers = usersWithNoticeCount
.sort((a, b) => b.noticeCount - a.noticeCount)
.slice(0, 5);
return (
<View className="mb-6">
<Heading className="text-2xl font-bold mb-4 mt-4">{title}</Heading>
<FlatList
data={topUsers}
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={{ paddingHorizontal: 8, gap: 12 }}
renderItem={({ item }) => {
return (
<UserBlock user={item} />
);
}}
/>
</View>
);
}

View File

@@ -0,0 +1,185 @@
'use client';
import React from 'react';
import { createAvatar } from '@gluestack-ui/avatar';
import { View, Text, Image, Platform } from 'react-native';
import { tva } from '@gluestack-ui/nativewind-utils/tva';
import {
withStyleContext,
useStyleContext,
} from '@gluestack-ui/nativewind-utils/withStyleContext';
const SCOPE = 'AVATAR';
import type { VariantProps } from '@gluestack-ui/nativewind-utils';
const UIAvatar = createAvatar({
Root: withStyleContext(View, SCOPE),
Badge: View,
Group: View,
Image: Image,
FallbackText: Text,
});
const avatarStyle = tva({
base: 'rounded-full justify-center items-center relative bg-primary-600 group-[.avatar-group]/avatar-group:-ml-2.5',
variants: {
size: {
'xs': 'w-6 h-6',
'sm': 'w-8 h-8',
'md': 'w-12 h-12',
'lg': 'w-16 h-16',
'xl': 'w-24 h-24',
'2xl': 'w-32 h-32',
},
},
});
const avatarFallbackTextStyle = tva({
base: 'text-typography-0 font-semibold overflow-hidden text-transform:uppercase web:cursor-default',
parentVariants: {
size: {
'xs': 'text-2xs',
'sm': 'text-xs',
'md': 'text-base',
'lg': 'text-xl',
'xl': 'text-3xl',
'2xl': 'text-5xl',
},
},
});
const avatarGroupStyle = tva({
base: 'group/avatar-group flex-row-reverse relative avatar-group',
});
const avatarBadgeStyle = tva({
base: 'w-5 h-5 bg-success-500 rounded-full absolute right-0 bottom-0 border-background-0 border-2',
parentVariants: {
size: {
'xs': 'w-2 h-2',
'sm': 'w-2 h-2',
'md': 'w-3 h-3',
'lg': 'w-4 h-4',
'xl': 'w-6 h-6',
'2xl': 'w-8 h-8',
},
},
});
const avatarImageStyle = tva({
base: 'h-full w-full rounded-full absolute',
});
type IAvatarProps = Omit<
React.ComponentPropsWithoutRef<typeof UIAvatar>,
'context'
> &
VariantProps<typeof avatarStyle>;
const Avatar = React.forwardRef<
React.ComponentRef<typeof UIAvatar>,
IAvatarProps
>(function Avatar({ className, size = 'md', ...props }, ref) {
return (
<UIAvatar
ref={ref}
{...props}
className={avatarStyle({ size, class: className })}
context={{ size }}
/>
);
});
type IAvatarBadgeProps = React.ComponentPropsWithoutRef<typeof UIAvatar.Badge> &
VariantProps<typeof avatarBadgeStyle>;
const AvatarBadge = React.forwardRef<
React.ComponentRef<typeof UIAvatar.Badge>,
IAvatarBadgeProps
>(function AvatarBadge({ className, size, ...props }, ref) {
const { size: parentSize } = useStyleContext(SCOPE);
return (
<UIAvatar.Badge
ref={ref}
{...props}
className={avatarBadgeStyle({
parentVariants: {
size: parentSize,
},
size,
class: className,
})}
/>
);
});
type IAvatarFallbackTextProps = React.ComponentPropsWithoutRef<
typeof UIAvatar.FallbackText
> &
VariantProps<typeof avatarFallbackTextStyle>;
const AvatarFallbackText = React.forwardRef<
React.ComponentRef<typeof UIAvatar.FallbackText>,
IAvatarFallbackTextProps
>(function AvatarFallbackText({ className, size, ...props }, ref) {
const { size: parentSize } = useStyleContext(SCOPE);
return (
<UIAvatar.FallbackText
ref={ref}
{...props}
className={avatarFallbackTextStyle({
parentVariants: {
size: parentSize,
},
size,
class: className,
})}
/>
);
});
type IAvatarImageProps = React.ComponentPropsWithoutRef<typeof UIAvatar.Image> &
VariantProps<typeof avatarImageStyle>;
const AvatarImage = React.forwardRef<
React.ComponentRef<typeof UIAvatar.Image>,
IAvatarImageProps
>(function AvatarImage({ className, ...props }, ref) {
return (
<UIAvatar.Image
ref={ref}
{...props}
className={avatarImageStyle({
class: className,
})}
// @ts-expect-error : This is a workaround to fix the issue with the image style on web.
style={
Platform.OS === 'web'
? { height: 'revert-layer', width: 'revert-layer' }
: undefined
}
/>
);
});
type IAvatarGroupProps = React.ComponentPropsWithoutRef<typeof UIAvatar.Group> &
VariantProps<typeof avatarGroupStyle>;
const AvatarGroup = React.forwardRef<
React.ComponentRef<typeof UIAvatar.Group>,
IAvatarGroupProps
>(function AvatarGroup({ className, ...props }, ref) {
return (
<UIAvatar.Group
ref={ref}
{...props}
className={avatarGroupStyle({
class: className,
})}
/>
);
});
export { Avatar, AvatarBadge, AvatarFallbackText, AvatarImage, AvatarGroup };

View File

@@ -0,0 +1,23 @@
import React from 'react';
import type { VariantProps } from '@gluestack-ui/nativewind-utils';
import { View } from 'react-native';
import type { ViewProps } from 'react-native';
import { hstackStyle } from './styles';
type IHStackProps = ViewProps & VariantProps<typeof hstackStyle>;
const HStack = React.forwardRef<React.ComponentRef<typeof View>, IHStackProps>(
function HStack({ className, space, reversed, ...props }, ref) {
return (
<View
className={hstackStyle({ space, reversed, class: className })}
{...props}
ref={ref}
/>
);
}
);
HStack.displayName = 'HStack';
export { HStack };

View File

@@ -0,0 +1,22 @@
import React from 'react';
import type { VariantProps } from '@gluestack-ui/nativewind-utils';
import { hstackStyle } from './styles';
type IHStackProps = React.ComponentPropsWithoutRef<'div'> &
VariantProps<typeof hstackStyle>;
const HStack = React.forwardRef<React.ComponentRef<'div'>, IHStackProps>(
function HStack({ className, space, reversed, ...props }, ref) {
return (
<div
className={hstackStyle({ space, reversed, class: className })}
{...props}
ref={ref}
/>
);
}
);
HStack.displayName = 'HStack';
export { HStack };

View File

@@ -0,0 +1,25 @@
import { isWeb } from '@gluestack-ui/nativewind-utils/IsWeb';
import { tva } from '@gluestack-ui/nativewind-utils/tva';
const baseStyle = isWeb
? 'flex relative z-0 box-border border-0 list-none min-w-0 min-h-0 bg-transparent items-stretch m-0 p-0 text-decoration-none'
: '';
export const hstackStyle = tva({
base: `flex-row ${baseStyle}`,
variants: {
space: {
'xs': 'gap-1',
'sm': 'gap-2',
'md': 'gap-3',
'lg': 'gap-4',
'xl': 'gap-5',
'2xl': 'gap-6',
'3xl': 'gap-7',
'4xl': 'gap-8',
},
reversed: {
true: 'flex-row-reverse',
},
},
});

View File

@@ -0,0 +1,39 @@
'use client';
import React from 'react';
import { createPressable } from '@gluestack-ui/pressable';
import { Pressable as RNPressable } from 'react-native';
import { tva } from '@gluestack-ui/nativewind-utils/tva';
import { withStyleContext } from '@gluestack-ui/nativewind-utils/withStyleContext';
import type { VariantProps } from '@gluestack-ui/nativewind-utils';
const UIPressable = createPressable({
Root: withStyleContext(RNPressable),
});
const pressableStyle = tva({
base: 'data-[focus-visible=true]:outline-none data-[focus-visible=true]:ring-indicator-info data-[focus-visible=true]:ring-2 data-[disabled=true]:opacity-40',
});
type IPressableProps = Omit<
React.ComponentProps<typeof UIPressable>,
'context'
> &
VariantProps<typeof pressableStyle>;
const Pressable = React.forwardRef<
React.ComponentRef<typeof UIPressable>,
IPressableProps
>(function Pressable({ className, ...props }, ref) {
return (
<UIPressable
{...props}
ref={ref}
className={pressableStyle({
class: className,
})}
/>
);
});
Pressable.displayName = 'Pressable';
export { Pressable };

View File

@@ -12,6 +12,7 @@
"@expo/vector-icons": "^14.1.0",
"@gluestack-style/react": "^1.0.57",
"@gluestack-ui/actionsheet": "^0.2.53",
"@gluestack-ui/avatar": "^0.1.18",
"@gluestack-ui/button": "^1.0.14",
"@gluestack-ui/form-control": "^0.1.19",
"@gluestack-ui/hstack": "^0.1.17",
@@ -20,6 +21,7 @@
"@gluestack-ui/input": "^0.1.38",
"@gluestack-ui/nativewind-utils": "^1.0.26",
"@gluestack-ui/overlay": "^0.1.22",
"@gluestack-ui/pressable": "^0.1.23",
"@gluestack-ui/select": "^0.1.31",
"@gluestack-ui/textarea": "^0.1.25",
"@gluestack-ui/themed": "^1.1.73",
@@ -2035,6 +2037,8 @@
},
"node_modules/@gluestack-ui/avatar": {
"version": "0.1.18",
"resolved": "https://registry.npmjs.org/@gluestack-ui/avatar/-/avatar-0.1.18.tgz",
"integrity": "sha512-VA9XwtavYLYCWrjxHc2u9gRpV97cPRcr/6KJ4tLiMiQbiRL1b4zckiL+/F39fB6xjUOUQHl3Fjo/Yd8swa0MBg==",
"dependencies": {
"@gluestack-ui/utils": "^0.1.14"
},
@@ -2246,6 +2250,8 @@
},
"node_modules/@gluestack-ui/pressable": {
"version": "0.1.23",
"resolved": "https://registry.npmjs.org/@gluestack-ui/pressable/-/pressable-0.1.23.tgz",
"integrity": "sha512-y7Sqwwe4+nIM5pECr3UT9qx7MMyuJHt1od6dfB/K+S2X91uZgTJEw7PUQgvOW6Jr8dBrStxFiTWfHmDqX/FVOQ==",
"dependencies": {
"@gluestack-ui/utils": "^0.1.15",
"@react-native-aria/focus": "^0.2.9",

View File

@@ -13,6 +13,7 @@
"@expo/vector-icons": "^14.1.0",
"@gluestack-style/react": "^1.0.57",
"@gluestack-ui/actionsheet": "^0.2.53",
"@gluestack-ui/avatar": "^0.1.18",
"@gluestack-ui/button": "^1.0.14",
"@gluestack-ui/form-control": "^0.1.19",
"@gluestack-ui/hstack": "^0.1.17",
@@ -21,6 +22,7 @@
"@gluestack-ui/input": "^0.1.38",
"@gluestack-ui/nativewind-utils": "^1.0.26",
"@gluestack-ui/overlay": "^0.1.22",
"@gluestack-ui/pressable": "^0.1.23",
"@gluestack-ui/select": "^0.1.31",
"@gluestack-ui/textarea": "^0.1.25",
"@gluestack-ui/themed": "^1.1.73",
@@ -31,6 +33,7 @@
"axios": "^1.9.0",
"babel-plugin-module-resolver": "^5.0.2",
"expo": "^53.0.0",
"expo-camera": "~16.1.6",
"expo-constants": "~17.1.5",
"expo-image-picker": "~16.1.4",
"expo-linking": "~7.1.4",
@@ -50,8 +53,7 @@
"react-native-svg": "15.11.2",
"react-native-web": "~0.20.0",
"tailwindcss": "^3.4.17",
"zustand": "^5.0.3",
"expo-camera": "~16.1.6"
"zustand": "^5.0.3"
},
"devDependencies": {
"@babel/core": "^7.20.0",