diff --git a/README.md b/README.md index 618ec8c..54d58d1 100644 --- a/README.md +++ b/README.md @@ -78,58 +78,73 @@ Aplikacja zawiera 3 przykładowych użytkowników: ## 📊 Funkcje -### ✅ Zaimplementowane (MVP) - -- ✅ **Autentykacja** - - Rejestracja użytkowników - - Logowanie (email + hasło) - - Wylogowanie - - Ochrona chronionych stron - -- ✅ **Baza danych** - - SQLite z Prisma ORM - - Modele: User, Post, Comment, Rating - - Seed z przykładowymi danymi - -### 🚧 W planach - -- 📝 Dodawanie ogłoszeń -- 🔍 Filtrowanie po kategorii i lokalizacji -- 💬 System komentarzy -- ⭐ Oceny użytkowników -- 🗺️ Mapa z lokalizacją ogłoszeń -- 👤 Profile użytkowników +### ✅ Funkcje Aplikacji + +- 📣 **System Ogłoszeń** + - Dodawanie, edycja i usuwanie ogłoszeń + - Przeglądanie listy ogłoszeń + - Szczegółowy widok ogłoszenia + +- 🔍 **Odkrywanie i Filtrowanie** + - Zaawansowane wyszukiwanie tekstowe + - Filtrowanie po kategorii + - Wyszukiwanie po lokalizacji + +- 💬 **Interakcje Społeczne** + - System komentarzy z aktualizacją w czasie rzeczywistym + - Oceny użytkowników (gwiazdki) i recenzje + - Historia ocen + +- 🗺️ **Integracja Mapy** + - Interaktywna mapa OpenStreetMap (Leaflet) + - Wybieranie lokalizacji przy dodawaniu ogłoszeń (Geocoding) + - Wizualizacja ogłoszeń na mapie strony głównej + +- 👤 **Profile Użytkowników** + - Publiczne profile użytkowników + - Historia aktywności i statystyki + - Edycja danych profilowych (bio, telefon, avatar) ## 📁 Struktura projektu ``` localaid/ ├── prisma/ -│ ├── schema.prisma # Modele bazy danych -│ ├── seed.js # Przykładowe dane -│ └── dev.db # SQLite database (88KB) +│ ├── schema.prisma # Modele bazy danych (User, Post, Comment, Rating) +│ ├── seed.js # Skrypt do seedowania danych +│ └── dev.db # Baza danych SQLite │ ├── src/ │ ├── app/ -│ │ ├── api/ # Backend API Routes -│ │ ├── auth/ # Strony autentykacji -│ │ ├── layout.tsx # Root layout -│ │ └── page.tsx # Strona główna +│ │ ├── api/ # Backend API (posts, users, comments, ratings) +│ │ ├── auth/ # Strony logowania i rejestracji +│ │ ├── posts/ # Strony ogłoszeń (create, [id], edit) +│ │ ├── profile/ # Strony profili użytkowników +│ │ ├── layout.tsx # Główny layout aplikacji +│ │ └── page.tsx # Strona główna z mapą i listą │ │ -│ ├── components/ # React components -│ │ └── providers/ +│ ├── components/ # Komponenty React +│ │ ├── CommentSection.tsx # Sekcja komentarzy +│ │ ├── LocationPicker.tsx # Wybór lokalizacji na mapie +│ │ ├── Map.tsx # Komponent mapy (Leaflet) +│ │ ├── MapWrapper.tsx # Wrapper mapy (Client-side) +│ │ ├── PostActions.tsx # Przyciski akcji (edycja/usuwanie) +│ │ ├── PostForm.tsx # Formularz ogłoszenia +│ │ ├── ProfileForm.tsx # Formularz edycji profilu +│ │ ├── SearchFilters.tsx # Pasek wyszukiwania i filtrów +│ │ └── UserRating.tsx # System oceniania │ │ │ ├── lib/ -│ │ ├── prisma.ts # Prisma client -│ │ ├── auth.ts # NextAuth config -│ │ └── utils.ts # Helper functions +│ │ ├── prisma.ts # Instancja klienta Prisma +│ │ ├── auth.ts # Konfiguracja NextAuth +│ │ └── utils.ts # Funkcje pomocnicze │ │ -│ ├── types/ # TypeScript types -│ └── constants/ # Kategorie i stałe +│ ├── types/ # Definicje typów TypeScript +│ └── constants/ # Stałe (kategorie, statusy) │ -├── .env # Konfiguracja (gitignored) -├── .env.example # Przykładowa konfiguracja -└── package.json +├── .env # Zmienne środowiskowe +├── .env.example # Szablon zmiennych środowiskowych +└── package.json # Zależności projektu ``` ## 🔧 Przydatne komendy diff --git a/package-lock.json b/package-lock.json index d93b375..f0398ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "react-leaflet": "^5.0.0", "react-toastify": "^11.0.5", "tailwind-merge": "^3.3.1", + "use-debounce": "^10.0.6", "zod": "^4.1.12" }, "devDependencies": { @@ -999,7 +1000,7 @@ "version": "6.17.1", "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.17.1.tgz", "integrity": "sha512-fs8wY6DsvOCzuiyWVckrVs1LOcbY4LZNz8ki4uUIQ28jCCzojTGqdLhN2Jl5lDnC1yI8/gNIKpsWDM8pLhOdwA==", - "devOptional": true, + "dev": true, "dependencies": { "c12": "3.1.0", "deepmerge-ts": "7.1.5", @@ -1011,13 +1012,13 @@ "version": "6.17.1", "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.17.1.tgz", "integrity": "sha512-Vf7Tt5Wh9XcndpbmeotuqOMLWPTjEKCsgojxXP2oxE1/xYe7PtnP76hsouG9vis6fctX+TxgmwxTuYi/+xc7dQ==", - "devOptional": true + "dev": true }, "node_modules/@prisma/engines": { "version": "6.17.1", "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.17.1.tgz", "integrity": "sha512-D95Ik3GYZkqZ8lSR4EyFOJ/tR33FcYRP8kK61o+WMsyD10UfJwd7+YielflHfKwiGodcqKqoraWw8ElAgMDbPw==", - "devOptional": true, + "dev": true, "hasInstallScript": true, "dependencies": { "@prisma/debug": "6.17.1", @@ -1030,13 +1031,13 @@ "version": "6.17.1-1.272a37d34178c2894197e17273bf937f25acdeac", "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.17.1-1.272a37d34178c2894197e17273bf937f25acdeac.tgz", "integrity": "sha512-17140E3huOuD9lMdJ9+SF/juOf3WR3sTJMVyyenzqUPbuH+89nPhSWcrY+Mf7tmSs6HvaO+7S+HkELinn6bhdg==", - "devOptional": true + "dev": true }, "node_modules/@prisma/fetch-engine": { "version": "6.17.1", "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.17.1.tgz", "integrity": "sha512-AYZiHOs184qkDMiTeshyJCtyL4yERkjfTkJiSJdYuSfc24m94lTNL5+GFinZ6vVz+ktX4NJzHKn1zIFzGTWrWg==", - "devOptional": true, + "dev": true, "dependencies": { "@prisma/debug": "6.17.1", "@prisma/engines-version": "6.17.1-1.272a37d34178c2894197e17273bf937f25acdeac", @@ -1047,7 +1048,7 @@ "version": "6.17.1", "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.17.1.tgz", "integrity": "sha512-AKEn6fsfz0r482S5KRDFlIGEaq9wLNcgalD1adL+fPcFFblIKs1sD81kY/utrHdqKuVC6E1XSRpegDK3ZLL4Qg==", - "devOptional": true, + "dev": true, "dependencies": { "@prisma/debug": "6.17.1" } @@ -1078,7 +1079,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", - "devOptional": true + "dev": true }, "node_modules/@standard-schema/utils": { "version": "0.3.0", @@ -1403,6 +1404,7 @@ "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.21.tgz", "integrity": "sha512-TbAd9DaPGSnzp6QvtYngntMZgcRk+igFELwR2N99XZn7RXUdKgsXMR+28bUO0rPsWp8MIu/f47luLIQuSLYv/w==", "dev": true, + "license": "MIT", "dependencies": { "@types/geojson": "*" } @@ -2264,7 +2266,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz", "integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==", - "devOptional": true, + "dev": true, "dependencies": { "chokidar": "^4.0.3", "confbox": "^0.2.2", @@ -2383,7 +2385,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "devOptional": true, + "dev": true, "dependencies": { "readdirp": "^4.0.1" }, @@ -2407,7 +2409,7 @@ "version": "0.1.6", "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", - "devOptional": true, + "dev": true, "dependencies": { "consola": "^3.2.3" } @@ -2453,13 +2455,13 @@ "version": "0.2.2", "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", - "devOptional": true + "dev": true }, "node_modules/consola": { "version": "3.4.2", "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", - "devOptional": true, + "dev": true, "engines": { "node": "^14.18.0 || >=16.10.0" } @@ -2545,6 +2547,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/kossnocorp" @@ -2577,7 +2580,7 @@ "version": "7.1.5", "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", - "devOptional": true, + "dev": true, "engines": { "node": ">=16.0.0" } @@ -2620,13 +2623,13 @@ "version": "6.1.4", "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", - "devOptional": true + "dev": true }, "node_modules/destr": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", - "devOptional": true + "dev": true }, "node_modules/detect-libc": { "version": "2.1.2", @@ -2653,7 +2656,7 @@ "version": "16.6.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "devOptional": true, + "dev": true, "engines": { "node": ">=12" }, @@ -2679,7 +2682,7 @@ "version": "3.16.12", "resolved": "https://registry.npmjs.org/effect/-/effect-3.16.12.tgz", "integrity": "sha512-N39iBk0K71F9nb442TLbTkjl24FLUzuvx2i1I2RsEAQsdAdUTuUoW0vlfUXgkMTUOnYqKnWcFfqw4hK4Pw27hg==", - "devOptional": true, + "dev": true, "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" @@ -2695,7 +2698,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", - "devOptional": true, + "dev": true, "engines": { "node": ">=14" } @@ -3301,13 +3304,13 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz", "integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==", - "devOptional": true + "dev": true }, "node_modules/fast-check": { "version": "3.23.2", "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", - "devOptional": true, + "dev": true, "funding": [ { "type": "individual", @@ -3571,7 +3574,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==", - "devOptional": true, + "dev": true, "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", @@ -4192,7 +4195,7 @@ "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", - "devOptional": true, + "dev": true, "bin": { "jiti": "lib/jiti-cli.mjs" } @@ -4298,7 +4301,8 @@ "node_modules/leaflet": { "version": "1.9.4", "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", - "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==" + "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", + "license": "BSD-2-Clause" }, "node_modules/levn": { "version": "0.4.1", @@ -4836,13 +4840,13 @@ "version": "1.6.7", "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", - "devOptional": true + "dev": true }, "node_modules/nypm": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.2.tgz", "integrity": "sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g==", - "devOptional": true, + "dev": true, "dependencies": { "citty": "^0.1.6", "consola": "^3.4.2", @@ -4984,7 +4988,7 @@ "version": "2.0.11", "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", - "devOptional": true + "dev": true }, "node_modules/optionator": { "version": "0.9.4", @@ -5090,13 +5094,13 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "devOptional": true + "dev": true }, "node_modules/perfect-debounce": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", - "devOptional": true + "dev": true }, "node_modules/picocolors": { "version": "1.1.1", @@ -5119,7 +5123,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", - "devOptional": true, + "dev": true, "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", @@ -5193,7 +5197,7 @@ "version": "6.17.1", "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.17.1.tgz", "integrity": "sha512-ac6h0sM1Tg3zu8NInY+qhP/S9KhENVaw9n1BrGKQVFu05JT5yT5Qqqmb8tMRIE3ZXvVj4xcRA5yfrsy4X7Yy5g==", - "devOptional": true, + "dev": true, "hasInstallScript": true, "dependencies": { "@prisma/config": "6.17.1", @@ -5238,7 +5242,7 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", - "devOptional": true, + "dev": true, "funding": [ { "type": "individual", @@ -5274,7 +5278,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", - "devOptional": true, + "dev": true, "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" @@ -5324,6 +5328,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-5.0.0.tgz", "integrity": "sha512-CWbTpr5vcHw5bt9i4zSlPEVQdTVcML390TjeDG0cK59z1ylexpqC6M1PJFjV8jD7CF+ACBFsLIDs6DRMoLEofw==", + "license": "Hippocratic-2.1", "dependencies": { "@react-leaflet/core": "^3.0.0" }, @@ -5349,7 +5354,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "devOptional": true, + "dev": true, "engines": { "node": ">= 14.18.0" }, @@ -5970,7 +5975,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.1.tgz", "integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==", - "devOptional": true + "dev": true }, "node_modules/tinyglobby": { "version": "0.2.15", @@ -6148,7 +6153,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "devOptional": true, + "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -6224,6 +6229,18 @@ "punycode": "^2.1.0" } }, + "node_modules/use-debounce": { + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-10.0.6.tgz", + "integrity": "sha512-C5OtPyhAZgVoteO9heXMTdW7v/IbFI+8bSVKYCJrSmiWWCLsbUxiBSp4t9v0hNBTGY97bT72ydDIDyGSFWfwXg==", + "license": "MIT", + "engines": { + "node": ">= 16.0.0" + }, + "peerDependencies": { + "react": "*" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 43d6405..23b97ba 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "react-leaflet": "^5.0.0", "react-toastify": "^11.0.5", "tailwind-merge": "^3.3.1", + "use-debounce": "^10.0.6", "zod": "^4.1.12" }, "devDependencies": { diff --git a/prisma/dev.db b/prisma/dev.db index 4684137..0661140 100644 Binary files a/prisma/dev.db and b/prisma/dev.db differ diff --git a/src/app/api/posts/[id]/comments/route.ts b/src/app/api/posts/[id]/comments/route.ts new file mode 100644 index 0000000..8da795f --- /dev/null +++ b/src/app/api/posts/[id]/comments/route.ts @@ -0,0 +1,90 @@ + +import { NextResponse } from "next/server"; +import { auth } from "@/lib/auth"; +import { prisma } from "@/lib/prisma"; +import { z } from "zod"; + +const commentSchema = z.object({ + content: z.string().min(1, "Komentarz nie może być pusty"), +}); + +export async function GET( + req: Request, + { params }: { params: Promise<{ id: string }> } +) { + try { + const { id: postId } = await params; + + const comments = await prisma.comment.findMany({ + where: { postId }, + include: { + author: { + select: { + name: true, + image: true, + } + } + }, + orderBy: { createdAt: "desc" }, + }); + + return NextResponse.json(comments); + } catch (error) { + console.error("Error fetching comments:", error); + return NextResponse.json( + { error: "Internal Server Error" }, + { status: 500 } + ); + } +} + +export async function POST( + req: Request, + { params }: { params: Promise<{ id: string }> } +) { + try { + const session = await auth(); + const { id: postId } = await params; + + if (!session || !session.user) { + return NextResponse.json( + { error: "Unauthorized" }, + { status: 401 } + ); + } + + const json = await req.json(); + const body = commentSchema.parse(json); + + const comment = await prisma.comment.create({ + data: { + content: body.content, + postId, + authorId: session.user.id, + }, + include: { + author: { + select: { + name: true, + image: true, + } + } + } + }); + + return NextResponse.json(comment, { status: 201 }); + } catch (error) { + if (error instanceof z.ZodError) { + return NextResponse.json( + { error: "Validation Error", details: error.issues }, + { status: 400 } + ); + } + + console.error("Error creating comment:", error); + return NextResponse.json( + { error: "Internal Server Error" }, + { status: 500 } + ); + } +} diff --git a/src/app/api/posts/[id]/route.ts b/src/app/api/posts/[id]/route.ts new file mode 100644 index 0000000..00c2543 --- /dev/null +++ b/src/app/api/posts/[id]/route.ts @@ -0,0 +1,157 @@ + +import { NextResponse } from "next/server"; +import { auth } from "@/lib/auth"; +import { prisma } from "@/lib/prisma"; +import { z } from "zod"; + +const postUpdateSchema = z.object({ + title: z.string().min(3).optional(), + description: z.string().min(10).optional(), + category: z.string().optional(), + status: z.enum(["active", "completed", "cancelled"]).optional(), + latitude: z.number().optional().nullable(), + longitude: z.number().optional().nullable(), + address: z.string().optional().nullable(), +}); + +export async function GET( + req: Request, + { params }: { params: Promise<{ id: string }> } +) { + try { + const { id } = await params; + + const post = await prisma.post.findUnique({ + where: { id }, + include: { + author: { + select: { + name: true, + image: true, + email: true, // Optional: might want to hide email depending on privacy + }, + }, + }, + }); + + if (!post) { + return NextResponse.json( + { error: "Post not found" }, + { status: 404 } + ); + } + + return NextResponse.json(post); + } catch (error) { + console.error("Error fetching post:", error); + return NextResponse.json( + { error: "Internal Server Error" }, + { status: 500 } + ); + } +} + +export async function PUT( + req: Request, + { params }: { params: Promise<{ id: string }> } +) { + try { + const session = await auth(); + const { id } = await params; + + if (!session || !session.user) { + return NextResponse.json( + { error: "Unauthorized" }, + { status: 401 } + ); + } + + const existingPost = await prisma.post.findUnique({ + where: { id }, + }); + + if (!existingPost) { + return NextResponse.json( + { error: "Post not found" }, + { status: 404 } + ); + } + + if (existingPost.authorId !== session.user.id) { + return NextResponse.json( + { error: "Forbidden" }, + { status: 403 } + ); + } + + const json = await req.json(); + const body = postUpdateSchema.parse(json); + + const updatedPost = await prisma.post.update({ + where: { id }, + data: body, + }); + + return NextResponse.json(updatedPost); + } catch (error) { + if (error instanceof z.ZodError) { + return NextResponse.json( + { error: "Validation Error", details: (error as z.ZodError).errors }, + { status: 400 } + ); + } + + console.error("Error updating post:", error); + return NextResponse.json( + { error: "Internal Server Error" }, + { status: 500 } + ); + } +} + +export async function DELETE( + req: Request, + { params }: { params: Promise<{ id: string }> } +) { + try { + const session = await auth(); + const { id } = await params; + + if (!session || !session.user) { + return NextResponse.json( + { error: "Unauthorized" }, + { status: 401 } + ); + } + + const existingPost = await prisma.post.findUnique({ + where: { id }, + }); + + if (!existingPost) { + return NextResponse.json( + { error: "Post not found" }, + { status: 404 } + ); + } + + if (existingPost.authorId !== session.user.id) { + return NextResponse.json( + { error: "Forbidden" }, + { status: 403 } + ); + } + + await prisma.post.delete({ + where: { id }, + }); + + return NextResponse.json({ message: "Post deleted successfully" }); + } catch (error) { + console.error("Error deleting post:", error); + return NextResponse.json( + { error: "Internal Server Error" }, + { status: 500 } + ); + } +} diff --git a/src/app/api/posts/route.ts b/src/app/api/posts/route.ts new file mode 100644 index 0000000..8691bd5 --- /dev/null +++ b/src/app/api/posts/route.ts @@ -0,0 +1,100 @@ + +import { NextResponse } from "next/server"; +import { auth } from "@/lib/auth"; +import { prisma } from "@/lib/prisma"; +import { z } from "zod"; + +const postSchema = z.object({ + title: z.string().min(3), + description: z.string().min(10), + category: z.string(), + latitude: z.number().optional().nullable(), + longitude: z.number().optional().nullable(), + address: z.string().optional().nullable(), +}); + +export async function GET(req: Request) { + try { + const { searchParams } = new URL(req.url); + const category = searchParams.get("category"); + + // Build where clause + const where: any = { + status: "active", + }; + + if (category && category !== "all") { + where.category = category; + } + + const search = searchParams.get("search"); + if (search) { + where.OR = [ + { title: { contains: search } }, + { description: { contains: search } }, + { address: { contains: search } }, + ]; + } + + const posts = await prisma.post.findMany({ + where, + include: { + author: { + select: { + name: true, + image: true, + }, + }, + }, + orderBy: { + createdAt: "desc", + }, + }); + + return NextResponse.json(posts); + } catch (error) { + console.error("Error fetching posts:", error); + return NextResponse.json( + { error: "Internal Server Error" }, + { status: 500 } + ); + } +} + +export async function POST(req: Request) { + try { + const session = await auth(); + + if (!session || !session.user) { + return NextResponse.json( + { error: "Unauthorized" }, + { status: 401 } + ); + } + + const json = await req.json(); + const body = postSchema.parse(json); + + const post = await prisma.post.create({ + data: { + ...body, + authorId: session.user.id, + }, + }); + + return NextResponse.json(post, { status: 201 }); + } catch (error) { + if (error instanceof z.ZodError) { + return NextResponse.json( + { error: "Validation Error", details: (error as z.ZodError).issues }, + { status: 400 } + ); + } + + console.error("Error creating post:", error); + return NextResponse.json( + { error: "Internal Server Error" }, + { status: 500 } + ); + } +} diff --git a/src/app/api/users/[id]/ratings/route.ts b/src/app/api/users/[id]/ratings/route.ts new file mode 100644 index 0000000..5c14110 --- /dev/null +++ b/src/app/api/users/[id]/ratings/route.ts @@ -0,0 +1,121 @@ + +import { NextResponse } from "next/server"; +import { auth } from "@/lib/auth"; +import { prisma } from "@/lib/prisma"; +import { z } from "zod"; + +const ratingSchema = z.object({ + rating: z.number().min(1).max(5), + comment: z.string().optional(), +}); + +export async function POST( + req: Request, + { params }: { params: Promise<{ id: string }> } +) { + try { + const session = await auth(); + const { id: reviewedId } = await params; + + if (!session || !session.user) { + return NextResponse.json( + { error: "Unauthorized" }, + { status: 401 } + ); + } + + if (session.user.id === reviewedId) { + return NextResponse.json( + { error: "Nie możesz ocenić samego siebie" }, + { status: 400 } + ); + } + + const json = await req.json(); + const body = ratingSchema.parse(json); + + // Check if rating already exists + const existingRating = await prisma.rating.findUnique({ + where: { + reviewerId_reviewedId: { + reviewerId: session.user.id, + reviewedId: reviewedId + } + } + }); + + if (existingRating) { + // Update existing rating + const updatedRating = await prisma.rating.update({ + where: { id: existingRating.id }, + data: { + rating: body.rating, + comment: body.comment + } + }); + return NextResponse.json(updatedRating); + } + + // Create new rating + const rating = await prisma.rating.create({ + data: { + rating: body.rating, + comment: body.comment, + reviewerId: session.user.id, + reviewedId: reviewedId, + }, + }); + + return NextResponse.json(rating, { status: 201 }); + } catch (error) { + if (error instanceof z.ZodError) { + return NextResponse.json( + { error: "Validation Error", details: error.issues }, + { status: 400 } + ); + } + + console.error("Error creating rating:", error); + return NextResponse.json( + { error: "Internal Server Error" }, + { status: 500 } + ); + } +} + +export async function GET( + req: Request, + { params }: { params: Promise<{ id: string }> } +) { + try { + const { id: userId } = await params; + + const ratings = await prisma.rating.findMany({ + where: { reviewedId: userId }, + include: { + reviewer: { + select: { + name: true, + image: true, + } + } + }, + orderBy: { createdAt: "desc" } + }); + + // Calculate average + const average = ratings.reduce((acc, curr) => acc + curr.rating, 0) / (ratings.length || 1); + + return NextResponse.json({ + ratings, + average: ratings.length > 0 ? average : 0, + count: ratings.length + }); + } catch (error) { + console.error("Error fetching ratings:", error); + return NextResponse.json( + { error: "Internal Server Error" }, + { status: 500 } + ); + } +} diff --git a/src/app/api/users/[id]/route.ts b/src/app/api/users/[id]/route.ts new file mode 100644 index 0000000..6fdb02c --- /dev/null +++ b/src/app/api/users/[id]/route.ts @@ -0,0 +1,57 @@ + +import { NextResponse } from "next/server"; +import { auth } from "@/lib/auth"; +import { prisma } from "@/lib/prisma"; +import { z } from "zod"; + +const profileSchema = z.object({ + name: z.string().min(2), + bio: z.string().optional().nullable(), + phone: z.string().optional().nullable(), + image: z.string().optional().nullable(), +}); + +export async function PUT( + req: Request, + { params }: { params: Promise<{ id: string }> } +) { + try { + const session = await auth(); + const { id: userId } = await params; + + if (!session || !session.user || session.user.id !== userId) { + return NextResponse.json( + { error: "Unauthorized" }, + { status: 401 } + ); + } + + const json = await req.json(); + const body = profileSchema.parse(json); + + const updatedUser = await prisma.user.update({ + where: { id: userId }, + data: { + name: body.name, + bio: body.bio, + phone: body.phone, + image: body.image, + }, + }); + + return NextResponse.json(updatedUser); + } catch (error) { + if (error instanceof z.ZodError) { + return NextResponse.json( + { error: "Validation Error", details: error.issues }, + { status: 400 } + ); + } + + console.error("Error updating user:", error); + return NextResponse.json( + { error: "Internal Server Error" }, + { status: 500 } + ); + } +} diff --git a/src/app/page.tsx b/src/app/page.tsx index be62a83..2eaee67 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,20 +1,64 @@ + import { auth } from "@/lib/auth" import { prisma } from "@/lib/prisma" import Link from "next/link" import { signOut } from "@/lib/auth" +import SearchFilters from "@/components/SearchFilters" +import { POST_CATEGORIES } from "@/constants/categories" +import MapWrapper from "@/components/MapWrapper"; + +export const dynamic = 'force-dynamic' -export default async function Home() { +export default async function Home( + props: { + searchParams: Promise<{ [key: string]: string | string[] | undefined }> + } +) { + const searchParams = await props.searchParams; const session = await auth() - - // Fetch some posts from database + + const category = searchParams.category as string | undefined; + const search = searchParams.search as string | undefined; + + const where: any = { + status: "active", + }; + + if (category && category !== "all") { + where.category = category; + } + + if (search) { + where.OR = [ + { title: { contains: search } }, + { description: { contains: search } }, + { address: { contains: search } }, + ]; + } + + // Fetch posts from database const posts = await prisma.post.findMany({ - take: 3, + where, orderBy: { createdAt: 'desc' }, - include: { + select: { + id: true, + title: true, + description: true, + category: true, + status: true, + createdAt: true, + address: true, + latitude: true, + longitude: true, author: { select: { name: true, email: true, + ratingsReceived: { + select: { + rating: true + } + } } }, _count: { @@ -38,6 +82,12 @@ export default async function Home() { Cześć, {session.user?.name || session.user?.email} + + Mój profil +