From cd78d536e594c2f74f1b3b5a4d78ce5b9836e5c3 Mon Sep 17 00:00:00 2001 From: ratulhasan Date: Fri, 3 May 2024 11:38:13 +0600 Subject: [PATCH 01/18] =?UTF-8?q?=F0=9F=9B=A0=EF=B8=8F=20[FIX]=20500=20err?= =?UTF-8?q?or=20when=20updating=20idea=20status?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Http/Controllers/Admin/FeedbackController.php | 8 ++++---- app/Models/Post.php | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/Http/Controllers/Admin/FeedbackController.php b/app/Http/Controllers/Admin/FeedbackController.php index 942d625..6b9b0b2 100644 --- a/app/Http/Controllers/Admin/FeedbackController.php +++ b/app/Http/Controllers/Admin/FeedbackController.php @@ -141,10 +141,6 @@ public function update(Request $request, Post $post) 'body' => $request->input('comment') ?? '', 'status_id' => $request->input('status_id'), ]); - - if ($request->input('notify') === true) { - $this->notify($post); - } } $post->update([ @@ -152,6 +148,10 @@ public function update(Request $request, Post $post) 'status_id' => $request->input('status_id'), ]); + if ($request->input('notify') === true) { + $this->notify($post); + } + return redirect()->back()->with('success', 'Feedback updated successfully.'); } diff --git a/app/Models/Post.php b/app/Models/Post.php index 7ef238d..f3decab 100644 --- a/app/Models/Post.php +++ b/app/Models/Post.php @@ -63,7 +63,7 @@ public function board() public function status() { - return $this->hasOne(Status::class, 'id', 'status_id'); + return $this->hasOne(Status::class, 'id', 'status_id')->withDefault(); } public function votes() From 5430754d6507cebfdfae682d4c5ad3742b8729fe Mon Sep 17 00:00:00 2001 From: ratulhasan Date: Fri, 3 May 2024 23:04:38 +0600 Subject: [PATCH 02/18] Implement search feature --- .../Controllers/Frontend/BoardController.php | 4 ++++ resources/js/Pages/Frontend/Board/Show.tsx | 21 +++++++++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/app/Http/Controllers/Frontend/BoardController.php b/app/Http/Controllers/Frontend/BoardController.php index 9a625e8..1c9df47 100644 --- a/app/Http/Controllers/Frontend/BoardController.php +++ b/app/Http/Controllers/Frontend/BoardController.php @@ -37,6 +37,10 @@ public function show(Request $request, Board $board) ]); } + if ($request->has('search')) { + $postsQuery->where('title', 'like', '%' . $request->search . '%'); + } + if ($request->has('sort') && in_array($request->sort, array_keys($sortFields))) { $orderBy = $sortFields[$request->sort]; } diff --git a/resources/js/Pages/Frontend/Board/Show.tsx b/resources/js/Pages/Frontend/Board/Show.tsx index d9866c5..55b3896 100644 --- a/resources/js/Pages/Frontend/Board/Show.tsx +++ b/resources/js/Pages/Frontend/Board/Show.tsx @@ -24,6 +24,7 @@ const ShowBoard = ({ auth, posts, board }: PageProps) => { const urlParams = new URLSearchParams(window.location.search); const sort = urlParams.get('sort'); const [sortKey, setSortKey] = useState(sort || 'voted'); + const [searchParam, setSearchParam] = useState(urlParams.get('search') || ''); const toggleVote = (post: PostType) => { if (!auth.user) { @@ -61,6 +62,16 @@ const ShowBoard = ({ auth, posts, board }: PageProps) => { ); }; + const handleSearch = () => { + router.visit( + route('board.show', { + board: board.slug, search: searchParam + }), + { + replace: true + }); + } + return (
@@ -93,8 +104,14 @@ const ShowBoard = ({ auth, posts, board }: PageProps) => { setSearchParam(e.target.value)} + className="px-4 pl-9 py-2 dark:bg-gray-800 rounded border-0 text-sm ring-1 ring-indigo-50 dark:ring-gray-700 focus:outline-none focus:ring-1 dark:text-gray-300" + onKeyDown={(e) => { + if (e.key === 'Enter' && searchParam) { + handleSearch(); + } + }} />
From ff98aba4a6f9cfa4ca7d9d5173467e8fc727da8a Mon Sep 17 00:00:00 2001 From: ratulhasan Date: Sat, 4 May 2024 00:28:48 +0600 Subject: [PATCH 03/18] =?UTF-8?q?=F0=9F=9B=A0=EF=B8=8F=20[FIX]=20Url=20par?= =?UTF-8?q?ams=20for=20all=20kinds=20of=20search=20and=20sorting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controllers/Frontend/BoardController.php | 2 +- resources/js/Pages/Frontend/Board/Show.tsx | 88 +++++++++++++++---- 2 files changed, 71 insertions(+), 19 deletions(-) diff --git a/app/Http/Controllers/Frontend/BoardController.php b/app/Http/Controllers/Frontend/BoardController.php index 1c9df47..ac532fb 100644 --- a/app/Http/Controllers/Frontend/BoardController.php +++ b/app/Http/Controllers/Frontend/BoardController.php @@ -37,7 +37,7 @@ public function show(Request $request, Board $board) ]); } - if ($request->has('search')) { + if ($request->has('search') && $request->search) { $postsQuery->where('title', 'like', '%' . $request->search . '%'); } diff --git a/resources/js/Pages/Frontend/Board/Show.tsx b/resources/js/Pages/Frontend/Board/Show.tsx index 55b3896..f7831ee 100644 --- a/resources/js/Pages/Frontend/Board/Show.tsx +++ b/resources/js/Pages/Frontend/Board/Show.tsx @@ -2,8 +2,7 @@ import React, { useState } from 'react'; import { Head, Link, router } from '@inertiajs/react'; import { MagnifyingGlassIcon, - ChevronUpIcon, - ChatBubbleLeftIcon, + ChatBubbleLeftIcon, ArrowPathIcon } from '@heroicons/react/24/outline'; import FrontendLayout from '@/Layouts/FrontendLayout'; @@ -16,6 +15,11 @@ type Props = { posts: PostType[]; board: BoardType; }; +type UrlParams = { + board: string; + search?: string; + sort?: string; +}; const ShowBoard = ({ auth, posts, board }: PageProps) => { const [allPosts, setAllPosts] = useState(posts); @@ -23,9 +27,11 @@ const ShowBoard = ({ auth, posts, board }: PageProps) => { // get sort key from url param const urlParams = new URLSearchParams(window.location.search); const sort = urlParams.get('sort'); - const [sortKey, setSortKey] = useState(sort || 'voted'); - const [searchParam, setSearchParam] = useState(urlParams.get('search') || ''); - + const search = urlParams.get('search'); + const [searchUrlParam, setSearchUrlParam] = useState({ + search: search || '', + sort: sort || 'voted', + }); const toggleVote = (post: PostType) => { if (!auth.user) { return; @@ -49,13 +55,22 @@ const ShowBoard = ({ auth, posts, board }: PageProps) => { const handleSortChange = (e: React.ChangeEvent) => { const value = e.target.value; - setSortKey(value); + setSearchUrlParam({ + ...searchUrlParam, + sort: value, + }); + let params: UrlParams = { + board: board.slug, + sort: value, + search: searchUrlParam.search + }; + + if (searchUrlParam.search.length > 0) { + delete params['search']; + } router.visit( - route('board.show', { - board: board.slug, - sort: value, - }), + route('board.show', params), { replace: true, } @@ -63,10 +78,16 @@ const ShowBoard = ({ auth, posts, board }: PageProps) => { }; const handleSearch = () => { + let params: UrlParams = { + board: board.slug, + sort: searchUrlParam.sort, + search: searchUrlParam.search, + }; + if (searchUrlParam.search.length === 0) { + delete params['search']; + } router.visit( - route('board.show', { - board: board.slug, search: searchParam - }), + route('board.show', params), { replace: true }); @@ -88,7 +109,7 @@ const ShowBoard = ({ auth, posts, board }: PageProps) => { setSearchParam(e.target.value)} + value={searchUrlParam.search} + onChange={(e) => { + setSearchUrlParam({ + ...searchUrlParam, + search: e.target.value, + }); + }} className="px-4 pl-9 py-2 dark:bg-gray-800 rounded border-0 text-sm ring-1 ring-indigo-50 dark:ring-gray-700 focus:outline-none focus:ring-1 dark:text-gray-300" onKeyDown={(e) => { - if (e.key === 'Enter' && searchParam) { + if (e.key === 'Enter') { handleSearch(); } }} + autoFocus={searchUrlParam.search.length > 0} />
+ {(search || searchUrlParam.search.length > 0) || (searchUrlParam.sort !== 'voted') ? ( +
+ +
+ ) : null}
From 32227e1e7c8f8c530dd36f35d56a579d11035230 Mon Sep 17 00:00:00 2001 From: ratulhasan Date: Sat, 4 May 2024 00:42:14 +0600 Subject: [PATCH 04/18] =?UTF-8?q?=F0=9F=9B=A0=EF=B8=8F=20[FIX]=20sorting?= =?UTF-8?q?=20issue?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- resources/js/Pages/Frontend/Board/Show.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/js/Pages/Frontend/Board/Show.tsx b/resources/js/Pages/Frontend/Board/Show.tsx index f7831ee..281f373 100644 --- a/resources/js/Pages/Frontend/Board/Show.tsx +++ b/resources/js/Pages/Frontend/Board/Show.tsx @@ -65,7 +65,7 @@ const ShowBoard = ({ auth, posts, board }: PageProps) => { search: searchUrlParam.search }; - if (searchUrlParam.search.length > 0) { + if (searchUrlParam.search.length === 0) { delete params['search']; } From 2d7e547b538c0f32e0895946beeed6b277a31726 Mon Sep 17 00:00:00 2001 From: ratulhasan Date: Wed, 8 May 2024 21:49:31 +0600 Subject: [PATCH 05/18] =?UTF-8?q?=F0=9F=9B=A0=EF=B8=8F=20[FIX]=20Merge=20p?= =?UTF-8?q?osts=20with=20parent?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controllers/Admin/FeedbackController.php | 49 ++++++ app/Models/Comment.php | 2 +- app/Models/Post.php | 3 +- app/Models/Vote.php | 2 +- ..._add_archive_post_id_to_comments_table.php | 28 ++++ ...36_add_archived_by_post_to_posts_table.php | 28 ++++ ...921_add_archive_post_id_to_votes_table.php | 28 ++++ resources/js/Components/ActionMenu.tsx | 56 +++++++ .../Pages/Admin/Feedbacks/MergeFeedback.tsx | 152 ++++++++++++++++++ resources/js/Pages/Admin/Feedbacks/Show.tsx | 66 ++++---- routes/web.php | 2 + 11 files changed, 384 insertions(+), 32 deletions(-) create mode 100644 database/migrations/2024_05_07_090550_add_archive_post_id_to_comments_table.php create mode 100644 database/migrations/2024_05_07_091836_add_archived_by_post_to_posts_table.php create mode 100644 database/migrations/2024_05_07_091921_add_archive_post_id_to_votes_table.php create mode 100644 resources/js/Components/ActionMenu.tsx create mode 100644 resources/js/Pages/Admin/Feedbacks/MergeFeedback.tsx diff --git a/app/Http/Controllers/Admin/FeedbackController.php b/app/Http/Controllers/Admin/FeedbackController.php index 6b9b0b2..8afeb9d 100644 --- a/app/Http/Controllers/Admin/FeedbackController.php +++ b/app/Http/Controllers/Admin/FeedbackController.php @@ -53,6 +53,23 @@ public function index(Request $request) ]); } + public function search(Request $request) + { + $search = $request->input('search'); + + $query = Post::where('status_id', '!=', Status::where('name', 'Closed')->first()->id); + + if ($request->has('search') && $search) { + $query->where('title', 'like', '%' . $search . '%'); + } + + $query->orderBy('vote', 'desc'); + + $response = $query->get(); + + return response()->json($response); + } + public function show(Post $post) { $post->load('creator', 'board', 'status', 'by'); @@ -195,4 +212,36 @@ public function addVote(Post $post, Request $request) return redirect()->back()->with('success', 'Vote added successfully.'); } + + public function merge(Request $request) + { + $request->validate([ + 'post_id' => 'required|exists:posts,id', + 'merge_ids' => 'required|array|exists:posts,id', + ]); + + foreach ($request->merge_ids as $id) { + $post = Post::find($id); + if (!$post || $post->archived_by_post) { + continue; + } + + Comment::where('post_id', $id)->update( [ + 'post_id' => $request->post_id, + 'archive_post_id' => $id, + ]); + + Vote::where('post_id', $id)->update([ + 'post_id' => $request->post_id, + 'archive_post_id' => $id, + ]); + + $post->update([ + 'archived_by_post' => $request->post_id, + 'status_id' => Status::where('name', 'Closed')->first()->id, + ]); + } + + return redirect()->back()->with('success', 'Posts merged successfully.'); + } } diff --git a/app/Models/Comment.php b/app/Models/Comment.php index 15e032b..adf922d 100644 --- a/app/Models/Comment.php +++ b/app/Models/Comment.php @@ -9,7 +9,7 @@ class Comment extends Model { use HasFactory; - protected $fillable = ['post_id', 'parent_id', 'status_id', 'user_id', 'body']; + protected $fillable = ['post_id', 'parent_id', 'status_id', 'user_id', 'body', 'archive_post_id']; protected static function boot() { diff --git a/app/Models/Post.php b/app/Models/Post.php index f3decab..9db135a 100644 --- a/app/Models/Post.php +++ b/app/Models/Post.php @@ -23,7 +23,8 @@ class Post extends Model 'eta', 'impact', 'effort', - 'created_by' + 'created_by', + 'archived_by_post_id', ]; protected static function boot() diff --git a/app/Models/Vote.php b/app/Models/Vote.php index 5246399..a5cd659 100644 --- a/app/Models/Vote.php +++ b/app/Models/Vote.php @@ -9,7 +9,7 @@ class Vote extends Model { use HasFactory; - protected $fillable = ['board_id', 'post_id', 'user_id', 'created_by']; + protected $fillable = ['board_id', 'post_id', 'user_id', 'created_by', 'archive_post_id']; protected static function boot() { diff --git a/database/migrations/2024_05_07_090550_add_archive_post_id_to_comments_table.php b/database/migrations/2024_05_07_090550_add_archive_post_id_to_comments_table.php new file mode 100644 index 0000000..569acef --- /dev/null +++ b/database/migrations/2024_05_07_090550_add_archive_post_id_to_comments_table.php @@ -0,0 +1,28 @@ +foreignId('archive_post_id')->after('status_id')->nullable()->constrained('posts')->onDelete('set null'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('comments', function (Blueprint $table) { + $table->dropConstrainedForeignId('archive_post_id'); + }); + } +}; diff --git a/database/migrations/2024_05_07_091836_add_archived_by_post_to_posts_table.php b/database/migrations/2024_05_07_091836_add_archived_by_post_to_posts_table.php new file mode 100644 index 0000000..d91650c --- /dev/null +++ b/database/migrations/2024_05_07_091836_add_archived_by_post_to_posts_table.php @@ -0,0 +1,28 @@ +foreignId('archived_by_post')->nullable()->constrained('posts')->onDelete('set null'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('posts', function (Blueprint $table) { + $table->dropConstrainedForeignId('archived_by_post'); + }); + } +}; diff --git a/database/migrations/2024_05_07_091921_add_archive_post_id_to_votes_table.php b/database/migrations/2024_05_07_091921_add_archive_post_id_to_votes_table.php new file mode 100644 index 0000000..2e946f7 --- /dev/null +++ b/database/migrations/2024_05_07_091921_add_archive_post_id_to_votes_table.php @@ -0,0 +1,28 @@ +foreignId('archive_post_id')->after( 'created_by')->nullable()->constrained('posts')->onDelete('set null'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('votes', function (Blueprint $table) { + $table->dropConstrainedForeignId('archive_post_id'); + }); + } +}; diff --git a/resources/js/Components/ActionMenu.tsx b/resources/js/Components/ActionMenu.tsx new file mode 100644 index 0000000..8224df8 --- /dev/null +++ b/resources/js/Components/ActionMenu.tsx @@ -0,0 +1,56 @@ +import { Fragment } from 'react' +import { Menu, Transition } from '@headlessui/react' +import { EllipsisVerticalIcon } from '@heroicons/react/20/solid' +import classNames from 'classnames'; + +type ActionMenuProps = { + menuItems: { + label: string; + onClick: () => void; + }[]; + menuName?: string; +}; + +export default function ActionMenu({ menuItems, menuName = 'Actions' }: ActionMenuProps) { + return ( + +
+ + Open options + {menuName} + +
+ + + +
+ {menuItems.map((item) => ( + + {({ active }) => ( + + )} + + ))} +
+
+
+
+ ) +} diff --git a/resources/js/Pages/Admin/Feedbacks/MergeFeedback.tsx b/resources/js/Pages/Admin/Feedbacks/MergeFeedback.tsx new file mode 100644 index 0000000..3613025 --- /dev/null +++ b/resources/js/Pages/Admin/Feedbacks/MergeFeedback.tsx @@ -0,0 +1,152 @@ +import { PostType } from '@/types'; +import { useForm } from '@inertiajs/react'; +import { + Button, + Modal, + ModalActions, + ModalBody, + ModalHeader, + TextField, + Textarea, +} from '@wedevs/tail-react'; +import React, { useCallback, useEffect, useState } from 'react'; +import { debounce } from 'lodash'; +import axios from 'axios'; + +type Props = { + post: PostType; + isOpen: boolean; + onClose: () => void; + onUpdate?: () => void; +}; + +const MergeFeedback: React.FC = ({ isOpen, onClose, post, onUpdate }) => { + const form = useForm({ + mergedPosts: [] as number[] + }); + const [search, setSearch] = useState(''); + const [searchedData, setSearchedData] = useState([] as PostType[]); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + + form.post( + route('admin.feedbacks.merge', { + post_id: post.id, + merge_ids: form.data.mergedPosts, + }), + { + preserveScroll: true, + onSuccess: () => { + onClose(); + setSearchedData([] as PostType[]); + setSearch(''); + + if (onUpdate) { + onUpdate(); + } + }, + } + ); + }; + + const handleSearch = useCallback( debounce((search: string) => { + axios.get(route('admin.feedbacks.search', { + isApi: true, + search: search, + })).then((response) => { + setSearchedData(response.data); + }); + }, 500), []); + return ( + +
+ Merge Feedback with {post.title} + + { + setSearch(value); + handleSearch(value); + }} + /> +
+
+ Ideas +
+ {searchedData.length === 0 && ( +
+

+ No feedback found. +

+
+ )} + {searchedData && searchedData.map((feedback) => ( +
+
+ { + if (event.target.checked) { + form.setData('mergedPosts', [...form.data.mergedPosts, feedback.id]); + } else { + form.setData('mergedPosts', form.data.mergedPosts.filter(item => item !== feedback.id)); + } + }} + /> +
+
+ + + {feedback.body} - {feedback.by?.name} + +
+
+ ))} +
+
+
+
+ + + + +
+
+ ); +}; + +export default MergeFeedback; diff --git a/resources/js/Pages/Admin/Feedbacks/Show.tsx b/resources/js/Pages/Admin/Feedbacks/Show.tsx index 8a4754e..7487517 100644 --- a/resources/js/Pages/Admin/Feedbacks/Show.tsx +++ b/resources/js/Pages/Admin/Feedbacks/Show.tsx @@ -27,6 +27,8 @@ import classNames from 'classnames'; import UserSearchDropdown from '@/Components/UserSearchDropdown'; import CreateUserModal from '@/Components/CreateUserModal'; import EditFeedback from './EditFeedback'; +import ActionMenu from '@/Components/ActionMenu'; +import MergeFeedback from '@/Pages/Admin/Feedbacks/MergeFeedback'; type Props = { post: PostType; @@ -43,6 +45,7 @@ type VoteProps = { const FeedbackShow = ({ post, statuses, boards, votes }: Props) => { const [showEditForm, setShowEditForm] = useState(false); + const [showMergeForm, setShowMergeForm] = useState(false); const [localPost, setLocalPost] = useState(post); const [showVoteModal, setShowVoteModal] = useState(false); const form = useForm({ @@ -168,35 +171,34 @@ const FeedbackShow = ({ post, statuses, boards, votes }: Props) => {
- - + setShowMergeForm(true), + }, + { + label: 'Edit', + onClick: () => setShowEditForm(true), + }, + { + label: 'Delete', + onClick: () => { + if ( + confirm( + 'Are you sure you want to delete this feedback? This action cannot be undone.' + ) + ) { + form.delete( + route('admin.feedbacks.destroy', { + post: post.slug, + }) + ); + } + }, + }, + ]} + />
@@ -297,6 +299,12 @@ const FeedbackShow = ({ post, statuses, boards, votes }: Props) => { post={localPost} /> + setShowMergeForm(false)} + /> + setShowEditForm(false)} diff --git a/routes/web.php b/routes/web.php index b97066f..db522c1 100644 --- a/routes/web.php +++ b/routes/web.php @@ -38,8 +38,10 @@ Route::redirect('/', '/admin/feedbacks'); Route::get('/feedbacks', [FeedbackController::class, 'index'])->name('admin.feedbacks.index'); + Route::get('/feedbacks/search', [FeedbackController::class, 'search'])->name('admin.feedbacks.search'); Route::get('/feedbacks/{post}', [FeedbackController::class, 'show'])->name('admin.feedbacks.show'); Route::post('/feedbacks', [FeedbackController::class, 'store'])->name('admin.feedbacks.store'); + Route::post( '/feedbacks/merge', [FeedbackController::class, 'merge'])->name('admin.feedbacks.merge'); Route::post('/feedbacks/{post}', [FeedbackController::class, 'update'])->name('admin.feedbacks.update'); Route::put('/feedbacks/{post}/update', [FeedbackController::class, 'updateContent'])->name('admin.feedbacks.update-content'); Route::post('/feedbacks/{post}/vote', [FeedbackController::class, 'addVote'])->name('admin.feedbacks.vote'); From 1ba7a23ca1cc33abdbf74ffcb97a704ca658f414 Mon Sep 17 00:00:00 2001 From: ratulhasan Date: Wed, 8 May 2024 21:49:50 +0600 Subject: [PATCH 06/18] Improve dark mode --- resources/js/Components/Comments.tsx | 2 +- resources/js/Pages/Admin/Status.tsx | 2 +- resources/js/Pages/Admin/User/Index.tsx | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/resources/js/Components/Comments.tsx b/resources/js/Components/Comments.tsx index 828c55e..cdfeeb2 100644 --- a/resources/js/Components/Comments.tsx +++ b/resources/js/Components/Comments.tsx @@ -65,7 +65,7 @@ const Comments: React.FC = ({ post }) => {
Sort By