-
Notifications
You must be signed in to change notification settings - Fork 0
enhancement: Add Like Button to Itinerary Detail Page #182
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: development
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,6 +1,6 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { Button } from '@/components/ui/button' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useAuthContext } from '@/contexts/AuthContext' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { Share2, X } from 'lucide-react' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { Heart, Share2, X } from 'lucide-react' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import Link from 'next/link' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useEffect, useRef, useState } from 'react' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -33,6 +33,7 @@ export const ItineraryHeader = ({ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [emailInput, setEmailInput] = useState('') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [emails, setEmails] = useState<string[]>([]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const inputRef = useRef<HTMLInputElement>(null) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [isLiked, setIsLiked] = useState(false) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Halo, saya coba implementasi kode ini di local saya, sepertinya kamu lupa init value isLiked dengan benar. Buttonnya akan selalu tidak liked, meskipun sudah pernah dilike sebelumnya. Sepertinya untuk memperbaiki ini tidak hanya harus mengubah kode frontend, tetapi juga mengubah implementasi backend untuk menginclude informasi apakah itinerarynya sudah dilike user atau belum. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (typeof window !== 'undefined') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -166,6 +167,22 @@ export const ItineraryHeader = ({ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const toggleSaveItinerary = async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const response = await customFetch(`/itineraries/${data.id}/save`, { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| method: isLiked === true ? 'DELETE' : 'POST', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| credentials: 'include', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!response.success) throw Error(response.message) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setIsLiked(!isLiked) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| toast.error( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error instanceof Error ? error.message : 'An unexpected error occurred' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+170
to
+184
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Inconsistent error handling pattern. The error handling in For better consistency with the rest of the codebase, consider using the same error handling pattern: const toggleSaveItinerary = async () => {
try {
const response = await customFetch(`/itineraries/${data.id}/save`, {
method: isLiked === true ? 'DELETE' : 'POST',
credentials: 'include',
})
- if (!response.success) throw Error(response.message)
+ if (response.statusCode !== 200 && response.statusCode !== 201) throw Error(response.message)
setIsLiked(!isLiked)
} catch (error) {
toast.error(
error instanceof Error ? error.message : 'An unexpected error occurred'
)
}
}📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const renderAcceptedUsers = () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (isLoading) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return <div className="text-center py-4">Memuat...</div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -295,18 +312,18 @@ export const ItineraryHeader = ({ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {user?.id === data.userId && ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="absolute top-2 right-2 sm:top-4 sm:right-4 z-10 flex gap-2"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Button | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type="button" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| size="sm" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| variant="ghost" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| className="bg-white text-black rounded-xl shadow" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onClick={openInviteDialog} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Share2 className="w-6 h-6 text-[#004080]" /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </Button> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {user?.id === data.userId && ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="absolute top-2 right-2 sm:top-4 sm:right-4 z-10 flex gap-2"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {user?.id === data.userId ? ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Button | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type="button" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| size="sm" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| variant="ghost" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| className="bg-white text-black rounded-xl shadow" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onClick={openInviteDialog} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Share2 className="w-6 h-6 text-[#004080]" /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </Button> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Link | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| href={contingencyId ? `${contingencyId}/edit` : `${data.id}/edit`} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -319,22 +336,30 @@ export const ItineraryHeader = ({ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Edit | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </Button> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </Link> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {user?.id !== data.userId && ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="absolute top-2 right-2 sm:top-4 sm:right-4 z-10 flex"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Button | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onClick={duplicateItinerary} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| size="sm" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type="button" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| variant="ghost" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| className="bg-white text-[#004080] rounded-xl shadow" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Duplikasi dan Edit | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </Button> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) : ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Button | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type="button" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| size="sm" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| variant="ghost" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| className="bg-white text-black rounded-xl shadow" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onClick={toggleSaveItinerary} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Heart className="w-6 h-6 text-[#004080]" /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </Button> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+348
to
+350
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Add visual state indication to the heart icon. The heart icon doesn't visually indicate whether the itinerary is liked or not. Users won't be able to tell the current state at a glance. Add visual differentiation to the heart icon based on the <Button
type="button"
size="sm"
variant="ghost"
className="bg-white text-black rounded-xl shadow"
onClick={toggleSaveItinerary}
+ aria-label={isLiked ? "Unlike itinerary" : "Like itinerary"}
>
- <Heart className="w-6 h-6 text-[#004080]" />
+ <Heart className={`w-6 h-6 ${isLiked ? "fill-[#004080] text-[#004080]" : "text-[#004080]"}`} />
</Button>This will fill the heart icon when the itinerary is liked, providing clear visual feedback to users, and also adds an appropriate aria-label for accessibility. 📝 Committable suggestion
Suggested change
Comment on lines
+342
to
+350
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sebaiknya button likenya menggunakan komponen LikesButtton yang dipakai di Itinerary Card saja. Di sana sudah ada logic untuk toggle dan fetching ke backend. Implementasi kamu yang sekarang tidak membedakan apakah itinerarynya sudah dilike atau belum. Gambar hatinya tetap kosong meskipun berhasil melike. Selain itu, tidak ada penanda juga kalau proses like berhasil (kalau di LikesButton misalnya sudah ada logic untuk mengeluarkan toast apakah proses like berhasil atau tidak) |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Button | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onClick={duplicateItinerary} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| size="sm" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type="button" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| variant="ghost" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| className="bg-white text-[#004080] rounded-xl shadow" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Duplikasi dan Edit | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </Button> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Dialog | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| open={showModal || showInviteDialog} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing initialization of
isLikedstate.The
isLikedstate is initialized tofalse, but there's no logic to set its initial value based on whether the user has already liked the itinerary. This could lead to an inconsistent UI state where an already-liked itinerary appears unliked when the page loads.Consider adding a useEffect to fetch the initial liked status:
📝 Committable suggestion