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 +
{ "use server" @@ -76,7 +126,7 @@ export default async function Home() {
{/* Welcome Section */}
-

+

{session ? `Witaj ponownie! 👋` : 'Witamy w LocalAid! 🎉'}

@@ -92,51 +142,83 @@ export default async function Home() { )}

- {/* Stats */} -
-
-
{posts.length}
-
Aktywnych ogłoszeń
+ {/* Search & Stats */} +
+ + + {/* Map Section */} +
+
-
-
- {posts.reduce((sum, post) => sum + post._count.comments, 0)} + +
+
+
{posts.length}
+
Znalezionych ogłoszeń
-
Komentarzy
-
-
-
3
-
Użytkowników
- {/* Recent Posts */} + {/* Posts List */}
-

📋 Najnowsze ogłoszenia

-
- {posts.map((post) => ( -
-

{post.title}

-

{post.description}

-
- 👤 {post.author.name} - 📁 {post.category} - 💬 {post._count.comments} komentarzy - 🕒 {new Date(post.createdAt).toLocaleDateString('pl-PL')} -
-
- ))} +
+

+ {search || category ? '🔍 Wyniki wyszukiwania' : '📋 Najnowsze ogłoszenia'} +

+ {session && ( + + + Dodaj ogłoszenie + + )}
-
- {/* Database Connection Test */} -
-

- ✅ Baza danych działa poprawnie! -

-

- Połączenie z SQLite zostało nawiązane. Załadowano {posts.length} ogłoszenia. -

+ {posts.length === 0 ? ( +
+ Nie znaleziono ogłoszeń spełniających kryteria. +
+ ) : ( +
+ {posts.map((post) => { + const categoryLabel = POST_CATEGORIES.find(c => c.value === post.category)?.label || post.category; + const authorRatings = post.author.ratingsReceived || []; + const averageAuthorRating = authorRatings.length > 0 + ? authorRatings.reduce((acc, cur) => acc + cur.rating, 0) / authorRatings.length + : 0; + + return ( +
+ +

+ {post.title} +

+ +

{post.description}

+
+
+ 👤 {post.author.name} + {averageAuthorRating > 0 && ( + + ★ {averageAuthorRating.toFixed(1)} + + )} +
+ + {categoryLabel} + + 💬 {post._count.comments} komentarzy + 🕒 {new Date(post.createdAt).toLocaleDateString('pl-PL')} + {post.address && ( + 📍 {post.address} + )} +
+
+ ); + })} +
+ )}
diff --git a/src/app/posts/[id]/edit/page.tsx b/src/app/posts/[id]/edit/page.tsx new file mode 100644 index 0000000..f94e0b3 --- /dev/null +++ b/src/app/posts/[id]/edit/page.tsx @@ -0,0 +1,64 @@ + +import { auth } from "@/lib/auth"; +import { prisma } from "@/lib/prisma"; +import PostForm from "@/components/PostForm"; +import { notFound, redirect } from "next/navigation"; +import Link from "next/link"; + +interface EditPostPageProps { + params: Promise<{ id: string }>; +} + +export default async function EditPostPage({ params }: EditPostPageProps) { + const { id } = await params; + const session = await auth(); + + if (!session) { + redirect(`/auth/signin?callbackUrl=/posts/${id}/edit`); + } + + const post = await prisma.post.findUnique({ + where: { id }, + }); + + if (!post) { + notFound(); + } + + if (post.authorId !== session.user?.id) { + return ( +
+
+ Nie masz uprawnień do edycji tego ogłoszenia. +
+
+ ); + } + + return ( +
+
+ + ← Wróć do strony głównej + +
+

Edytuj ogłoszenie

+
+ + +
+
+ ); +} diff --git a/src/app/posts/[id]/page.tsx b/src/app/posts/[id]/page.tsx new file mode 100644 index 0000000..ed7156e --- /dev/null +++ b/src/app/posts/[id]/page.tsx @@ -0,0 +1,156 @@ + +import { auth } from "@/lib/auth"; +import { prisma } from "@/lib/prisma"; +import Link from "next/link"; +import { notFound } from "next/navigation"; +import { POST_CATEGORIES, POST_STATUSES } from "@/constants/categories"; +import PostActions from "@/components/PostActions"; +import CommentSection from "@/components/CommentSection"; +import UserRating from "@/components/UserRating"; + +export default async function PostPage( + props: { + params: Promise<{ id: string }>; + } +) { + const params = await props.params; + const session = await auth(); + + const post = await prisma.post.findUnique({ + where: { id: params.id }, + include: { + author: { + select: { + id: true, + name: true, + image: true, + email: true, + }, + }, + comments: { + include: { + author: { + select: { name: true, image: true }, + }, + }, + orderBy: { createdAt: "desc" }, + }, + }, + }); + + if (!post) { + notFound(); + } + + // Fetch ratings for the author + const ratings = await prisma.rating.findMany({ + where: { reviewedId: post.authorId }, + }); + + const averageRating = ratings.reduce((acc, curr) => acc + curr.rating, 0) / (ratings.length || 1); + const userRating = session?.user ? ratings.find(r => r.reviewerId === session.user?.id) : null; + const canRate = session?.user?.id !== post.authorId; + + const category = POST_CATEGORIES.find((c) => c.value === post.category); + const status = POST_STATUSES.find((s) => s.value === post.status); + const isAuthor = session?.user?.id === post.authorId; + + return ( +
+
+ + ← Wróć do strony głównej + + + {/* Post Header */} +
+
+
+ + {category?.label || post.category} + +

+ {post.title} +

+
+ 👤 {post.author.name} + 🕒 {new Date(post.createdAt).toLocaleDateString("pl-PL")} +
+
+ {isAuthor && ( + + )} +
+ +
+ {post.description} +
+ + {post.address && ( +
+

📍 Lokalizacja

+

{post.address}

+
+ )} +
+ + {/* Contact / Action Section */} +
+

📞 Kontakt

+ {session ? ( +
+

+ Skontaktuj się z autorem, aby zaoferować pomoc lub dopytać o szczegóły. +

+
+ +

{post.author.name}

+ + +

{post.author.email}

+
+
+ ) : ( +
+

Zaloguj się, aby zobaczyć dane kontaktowe.

+ + Zaloguj się + +
+ )} +
+ + {/* Comments Section */} + ({ + id: c.id, + content: c.content, + createdAt: c.createdAt.toISOString(), + author: { + name: c.author.name, + image: c.author.image, + } + }))} + isLoggedIn={!!session} + /> +
+
+ ); +} diff --git a/src/app/posts/create/page.tsx b/src/app/posts/create/page.tsx new file mode 100644 index 0000000..8def83e --- /dev/null +++ b/src/app/posts/create/page.tsx @@ -0,0 +1,34 @@ + +import { auth } from "@/lib/auth"; +import { redirect } from "next/navigation"; +import PostForm from "@/components/PostForm"; +import Link from "next/link"; + +export default async function CreatePostPage() { + const session = await auth(); + + if (!session) { + redirect("/auth/signin?callbackUrl=/posts/create"); + } + + return ( +
+
+ + ← Wróć do strony głównej + +
+

Dodaj nowe ogłoszenie

+

+ Wypełnij formularz, aby poprosić o pomoc lub zaoferować wsparcie. +

+
+ + +
+
+ ); +} diff --git a/src/app/profile/[id]/edit/page.tsx b/src/app/profile/[id]/edit/page.tsx new file mode 100644 index 0000000..6c85b9f --- /dev/null +++ b/src/app/profile/[id]/edit/page.tsx @@ -0,0 +1,48 @@ + +import { auth } from "@/lib/auth"; +import { prisma } from "@/lib/prisma"; +import { notFound, redirect } from "next/navigation"; +import ProfileForm from "@/components/ProfileForm"; + +export default async function EditProfilePage( + props: { + params: Promise<{ id: string }>; + } +) { + const params = await props.params; + const session = await auth(); + const userId = params.id; + + if (!session || session.user?.id !== userId) { + redirect("/auth/signin"); + } + + const user = await prisma.user.findUnique({ + where: { id: userId }, + }); + + if (!user) { + notFound(); + } + + return ( +
+
+
+

Edytuj profil

+

Zaktualizuj swoje dane, aby inni mogli Cię łatwiej znaleźć.

+
+ + +
+
+ ); +} diff --git a/src/app/profile/[id]/page.tsx b/src/app/profile/[id]/page.tsx new file mode 100644 index 0000000..0b6b971 --- /dev/null +++ b/src/app/profile/[id]/page.tsx @@ -0,0 +1,180 @@ + +import { auth } from "@/lib/auth"; +import { prisma } from "@/lib/prisma"; +import Link from "next/link"; +import { notFound } from "next/navigation"; +import { POST_CATEGORIES } from "@/constants/categories"; + +export default async function ProfilePage( + props: { + params: Promise<{ id: string }>; + } +) { + const params = await props.params; + const session = await auth(); + const userId = params.id; + + const user = await prisma.user.findUnique({ + where: { id: userId }, + include: { + posts: { + orderBy: { createdAt: "desc" }, + include: { + _count: { + select: { comments: true } + } + } + }, + ratingsReceived: { + include: { + reviewer: { + select: { name: true, image: true } + } + }, + orderBy: { createdAt: "desc" } + } + }, + }); + + if (!user) { + notFound(); + } + + const isOwnProfile = session?.user?.id === user.id; + const averageRating = + user.ratingsReceived.length > 0 + ? user.ratingsReceived.reduce((acc, curr) => acc + curr.rating, 0) / + user.ratingsReceived.length + : 0; + + return ( +
+
+ + + ← Wróć do strony głównej + + + {/* Profile Header */} +
+
+
+ {user.name?.[0] || user.email[0].toUpperCase()} +
+
+
+

{user.name}

+

{user.email}

+ {user.phone &&

📞 {user.phone}

} + {user.bio &&

"{user.bio}"

} + +
+
+ {averageRating.toFixed(1)} + +

Średnia ocena ({user.ratingsReceived.length})

+
+
+ {user.posts.length} +

Ogłoszeń

+
+
+
+ {isOwnProfile && ( +
+ + Edytuj profil + + + Wyloguj się + +
+ )} +
+ +
+ + {/* User Posts Column */} +
+

+ {isOwnProfile ? "Moje ogłoszenia" : `Ogłoszenia użytkownika ${user.name}`} +

+ + {user.posts.length === 0 ? ( +

Brak aktywnych ogłoszeń.

+ ) : ( + user.posts.map(post => { + const categoryLabel = POST_CATEGORIES.find(c => c.value === post.category)?.label || post.category; + return ( +
+
+ + {categoryLabel} + + + {post.status === 'active' ? 'Aktywne' : post.status} + +
+

+ + {post.title} + +

+

{post.description}

+
+ 📅 {new Date(post.createdAt).toLocaleDateString('pl-PL')} + 💬 {post._count.comments} komentarzy +
+
+ ) + }) + )} +
+ + {/* Ratings History Column */} +
+

Opinie

+ + {user.ratingsReceived.length === 0 ? ( +
+

Ten użytkownik nie ma jeszcze opinii.

+
+ ) : ( +
+ {user.ratingsReceived.map(rating => ( +
+
+ + {rating.reviewer.name || "Anonim"} + +
+ {"★".repeat(rating.rating)} + {"★".repeat(5 - rating.rating)} +
+
+ {rating.comment && ( +

"{rating.comment}"

+ )} +

+ {new Date(rating.createdAt).toLocaleDateString('pl-PL')} +

+
+ ))} +
+ )} +
+ +
+
+
+ ); +} diff --git a/src/components/CommentSection.tsx b/src/components/CommentSection.tsx new file mode 100644 index 0000000..cab1530 --- /dev/null +++ b/src/components/CommentSection.tsx @@ -0,0 +1,136 @@ + +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { format } from "date-fns"; +import { pl } from "date-fns/locale"; + +interface Comment { + id: string; + content: string; + createdAt: string; + author: { + name: string | null; + image: string | null; + }; +} + +interface CommentSectionProps { + postId: string; + initialComments: Comment[]; + isLoggedIn: boolean; +} + +export default function CommentSection({ + postId, + initialComments, + isLoggedIn, +}: CommentSectionProps) { + const router = useRouter(); + const [comments, setComments] = useState(initialComments); + const [content, setContent] = useState(""); + const [isSubmitting, setIsSubmitting] = useState(false); + const [error, setError] = useState(null); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!content.trim()) return; + + setIsSubmitting(true); + setError(null); + + try { + const res = await fetch(`/api/posts/${postId}/comments`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ content }), + }); + + if (!res.ok) { + throw new Error("Nie udało się dodać komentarza"); + } + + const newComment = await res.json(); + setComments([newComment, ...comments]); + setContent(""); + router.refresh(); + } catch (err: any) { + setError(err.message); + } finally { + setIsSubmitting(false); + } + }; + + return ( +
+

+ 💬 Komentarze ({comments.length}) +

+ + {isLoggedIn ? ( + +
+ +