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",