diff --git a/ArtisanConnect/app/(tabs)/index.jsx b/ArtisanConnect/app/(tabs)/index.jsx index 7c7e92a..b631c24 100644 --- a/ArtisanConnect/app/(tabs)/index.jsx +++ b/ArtisanConnect/app/(tabs)/index.jsx @@ -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 ( - - Home - - - - - - - - + + + + + + ); } diff --git a/ArtisanConnect/app/_layout.jsx b/ArtisanConnect/app/_layout.jsx index 2a97c77..7581fc8 100644 --- a/ArtisanConnect/app/_layout.jsx +++ b/ArtisanConnect/app/_layout.jsx @@ -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 ( diff --git a/ArtisanConnect/components/CategorySection.jsx b/ArtisanConnect/components/CategorySection.jsx new file mode 100644 index 0000000..db7e18d --- /dev/null +++ b/ArtisanConnect/components/CategorySection.jsx @@ -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 ( + + {title} + item} + horizontal + showsHorizontalScrollIndicator={false} + contentContainerStyle={{ paddingHorizontal: 8, gap: 12 }} + renderItem={({ item }) => { + const categoryObj = categoryMap.find((cat) => cat.value === item); + return ( + + + {categoryObj ? categoryObj.label : item} ({getCount(item)}) + + + ); + }} + /> + + ); + +} \ No newline at end of file diff --git a/ArtisanConnect/components/NoticeSection.jsx b/ArtisanConnect/components/NoticeSection.jsx new file mode 100644 index 0000000..478dbe7 --- /dev/null +++ b/ArtisanConnect/components/NoticeSection.jsx @@ -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( + + + {notices[i + 1] && } + + ); + } + return ( + + {title} + + {rows} + + + + ); +} \ No newline at end of file diff --git a/ArtisanConnect/components/UserBlock.jsx b/ArtisanConnect/components/UserBlock.jsx new file mode 100644 index 0000000..dbb51cf --- /dev/null +++ b/ArtisanConnect/components/UserBlock.jsx @@ -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 ( + + + + {user.firstName} {user.lastName} + + + {user.firstName} {user.lastName} + + + ); +} \ No newline at end of file diff --git a/ArtisanConnect/components/UserSection.jsx b/ArtisanConnect/components/UserSection.jsx new file mode 100644 index 0000000..db42d59 --- /dev/null +++ b/ArtisanConnect/components/UserSection.jsx @@ -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 ( + + {title} + { + return ( + + ); + }} + /> + + ); + +} \ No newline at end of file diff --git a/ArtisanConnect/components/ui/avatar/index.tsx b/ArtisanConnect/components/ui/avatar/index.tsx new file mode 100644 index 0000000..7bf9772 --- /dev/null +++ b/ArtisanConnect/components/ui/avatar/index.tsx @@ -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, + 'context' +> & + VariantProps; + +const Avatar = React.forwardRef< + React.ComponentRef, + IAvatarProps +>(function Avatar({ className, size = 'md', ...props }, ref) { + return ( + + ); +}); + +type IAvatarBadgeProps = React.ComponentPropsWithoutRef & + VariantProps; + +const AvatarBadge = React.forwardRef< + React.ComponentRef, + IAvatarBadgeProps +>(function AvatarBadge({ className, size, ...props }, ref) { + const { size: parentSize } = useStyleContext(SCOPE); + + return ( + + ); +}); + +type IAvatarFallbackTextProps = React.ComponentPropsWithoutRef< + typeof UIAvatar.FallbackText +> & + VariantProps; +const AvatarFallbackText = React.forwardRef< + React.ComponentRef, + IAvatarFallbackTextProps +>(function AvatarFallbackText({ className, size, ...props }, ref) { + const { size: parentSize } = useStyleContext(SCOPE); + + return ( + + ); +}); + +type IAvatarImageProps = React.ComponentPropsWithoutRef & + VariantProps; + +const AvatarImage = React.forwardRef< + React.ComponentRef, + IAvatarImageProps +>(function AvatarImage({ className, ...props }, ref) { + return ( + + ); +}); + +type IAvatarGroupProps = React.ComponentPropsWithoutRef & + VariantProps; + +const AvatarGroup = React.forwardRef< + React.ComponentRef, + IAvatarGroupProps +>(function AvatarGroup({ className, ...props }, ref) { + return ( + + ); +}); + +export { Avatar, AvatarBadge, AvatarFallbackText, AvatarImage, AvatarGroup }; diff --git a/ArtisanConnect/components/ui/hstack/index.tsx b/ArtisanConnect/components/ui/hstack/index.tsx new file mode 100644 index 0000000..0022bba --- /dev/null +++ b/ArtisanConnect/components/ui/hstack/index.tsx @@ -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; + +const HStack = React.forwardRef, IHStackProps>( + function HStack({ className, space, reversed, ...props }, ref) { + return ( + + ); + } +); + +HStack.displayName = 'HStack'; + +export { HStack }; diff --git a/ArtisanConnect/components/ui/hstack/index.web.tsx b/ArtisanConnect/components/ui/hstack/index.web.tsx new file mode 100644 index 0000000..51c3d3b --- /dev/null +++ b/ArtisanConnect/components/ui/hstack/index.web.tsx @@ -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; + +const HStack = React.forwardRef, IHStackProps>( + function HStack({ className, space, reversed, ...props }, ref) { + return ( +
+ ); + } +); + +HStack.displayName = 'HStack'; + +export { HStack }; diff --git a/ArtisanConnect/components/ui/hstack/styles.tsx b/ArtisanConnect/components/ui/hstack/styles.tsx new file mode 100644 index 0000000..f02fb7c --- /dev/null +++ b/ArtisanConnect/components/ui/hstack/styles.tsx @@ -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', + }, + }, +}); diff --git a/ArtisanConnect/components/ui/pressable/index.tsx b/ArtisanConnect/components/ui/pressable/index.tsx new file mode 100644 index 0000000..cf18697 --- /dev/null +++ b/ArtisanConnect/components/ui/pressable/index.tsx @@ -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, + 'context' +> & + VariantProps; +const Pressable = React.forwardRef< + React.ComponentRef, + IPressableProps +>(function Pressable({ className, ...props }, ref) { + return ( + + ); +}); + +Pressable.displayName = 'Pressable'; +export { Pressable }; diff --git a/ArtisanConnect/package-lock.json b/ArtisanConnect/package-lock.json index 0bfeb96..d736031 100644 --- a/ArtisanConnect/package-lock.json +++ b/ArtisanConnect/package-lock.json @@ -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", diff --git a/ArtisanConnect/package.json b/ArtisanConnect/package.json index 2e21222..29070cb 100644 --- a/ArtisanConnect/package.json +++ b/ArtisanConnect/package.json @@ -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",