diff --git a/frontend/vizzy/app/dashboard/dashboard-page-client.tsx b/frontend/vizzy/app/dashboard/dashboard-page-client.tsx index 103d95ee..43e37209 100644 --- a/frontend/vizzy/app/dashboard/dashboard-page-client.tsx +++ b/frontend/vizzy/app/dashboard/dashboard-page-client.tsx @@ -7,7 +7,6 @@ import { TabsList, TabsTrigger, } from '@/components/ui/navigation/tabs'; -import { CalendarDateRangePicker } from '@/components/ui/data-display/date-range-picker'; import { OverviewPage } from './layout/overview-page'; import { ListingsPage } from './layout/listings-page'; import { ProposalsPage } from './layout/proposals-page'; @@ -17,19 +16,42 @@ import { FilterDropdown, type FilterOption, } from '@/components/ui/data-display/filter-dropdown'; +import { useTranslations } from 'next-intl'; import { FavoritesPage } from './layout/favorites-page'; + export default function DashboardPageClient() { const searchParams = useSearchParams(); const tabParam = searchParams.get('activeTab'); const [activeTab, setActiveTab] = useState(tabParam || 'overview'); const [createListingOpen, setCreateListingOpen] = useState(false); + const t = useTranslations('dashboard'); const [filterOptions, setFilterOptions] = useState([ - { id: 'received', label: 'Received', checked: false }, - { id: 'sent', label: 'Sent', checked: false }, - { id: 'accepted', label: 'Accepted', checked: false }, - { id: 'rejected', label: 'Rejected', checked: false }, - { id: 'cancelled', label: 'Cancelled', checked: false }, - { id: 'pending', label: 'Pending', checked: true }, + { + id: 'received', + label: t('proposals.filterOptions.received'), + checked: false, + }, + { id: 'sent', label: t('proposals.filterOptions.sent'), checked: false }, + { + id: 'accepted', + label: t('proposals.filterOptions.accepted'), + checked: false, + }, + { + id: 'rejected', + label: t('proposals.filterOptions.rejected'), + checked: false, + }, + { + id: 'cancelled', + label: t('proposals.filterOptions.cancelled'), + checked: false, + }, + { + id: 'pending', + label: t('proposals.filterOptions.pending'), + checked: true, + }, ]); const [filterDropdownOpen, setFilterDropdownOpen] = useState(false); @@ -59,13 +81,9 @@ export default function DashboardPageClient() {
-

- Painel de Controlo -

-
- -
+

{t('title')}

+

{t('subtitle')}

- Visão Geral + {t('tabs.overview')} - Anúncios + {t('tabs.listings')} - Propostas + {t('tabs.proposals')} Favoritos @@ -93,7 +111,7 @@ export default function DashboardPageClient() { variant={'default'} onClick={() => setCreateListingOpen(true)} > - Novo Anúncio + {t('listings.button')} )} @@ -101,8 +119,8 @@ export default function DashboardPageClient() { ([]); + const [isLoadingContacts, setIsLoadingContacts] = useState(true); + const [contactsError, setContactsError] = useState(null); + useEffect(() => { const fetchUserContacts = async () => { - const contacts = await fetchContacts(profile.id); - if (contacts.data !== null) setContacts(contacts.data); + if (!profile?.id) return; + + try { + setIsLoadingContacts(true); + setContactsError(null); + const result = await fetchContacts(profile.id); + if (result.data !== null) { + setContacts(result.data); + } + } catch (error) { + console.error('Failed to fetch contacts:', error); + setContactsError('Failed to load contacts'); + } finally { + setIsLoadingContacts(false); + } }; + fetchUserContacts(); - }, [profile.id]); + }, [profile?.id]); + + // Helper function to get initials for avatar fallback + const getInitials = (name: string) => { + if (!name) return ''; + return name + .split(' ') + .map((n) => n[0]?.toUpperCase() || '') + .join('') + .slice(0, 2); + }; + + // Early return moved after hooks + if (!profile?.name || !profile?.username) { + return null; + } return ( @@ -45,17 +77,15 @@ export default function ProfileHoverCard({ profile }: { profile: Profile }) {
- - - {profile.name - .split(' ') - .map((name) => name[0].toUpperCase()) - .join('')} - + + {getInitials(profile.name)}
- +

@@ -63,12 +93,20 @@ export default function ProfileHoverCard({ profile }: { profile: Profile }) {

- {profile.location.slice(0, 30) + '...'} + + {profile.location + ? `${profile.location.slice(0, 30)}...` + : t('location.notAvailable')} +
- {profile.totalSales} {t('stats.totalTransactions.label')} + {typeof profile.totalSales === 'number' + ? `${profile.totalSales} ${t( + 'stats.totalTransactions.label', + )}` + : t('stats.noTransactions')}
@@ -77,19 +115,25 @@ export default function ProfileHoverCard({ profile }: { profile: Profile }) { variant="secondary" className="shrink-0 text-xs font-medium opacity-90" > - {'Verified'} + {t('header.verifiedBadge')} )}

- {t('stats.memberSince.label')}: {profile.memberSince} + {profile.memberSince + ? `${t('stats.memberSince.label')}: ${profile.memberSince}` + : t('stats.memberSinceNotAvailable')}

- {contacts[0] + {isLoadingContacts + ? t('contacts.loading') + : contactsError + ? t('contacts.error') + : contacts[0] ? `${contacts[0].name} (${contacts[0].phone_number})` - : 'No contacts'} + : t('contacts.noContacts')}

diff --git a/frontend/vizzy/components/ui/data-display/filter-dropdown.tsx b/frontend/vizzy/components/ui/data-display/filter-dropdown.tsx index bb08da99..c6b3e9ff 100644 --- a/frontend/vizzy/components/ui/data-display/filter-dropdown.tsx +++ b/frontend/vizzy/components/ui/data-display/filter-dropdown.tsx @@ -1,8 +1,8 @@ -"use client" -import { Check, Filter } from "lucide-react" -import { useState, useEffect } from "react" +'use client'; +import { Check, Filter } from 'lucide-react'; +import { useState, useEffect } from 'react'; -import { Button } from "@/components/ui/common/button" +import { Button } from '@/components/ui/common/button'; import { DropdownMenu, DropdownMenuCheckboxItem, @@ -10,76 +10,88 @@ import { DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, -} from "@/components/ui/overlay/dropdown-menu" -import { Badge } from "@/components/ui/common/badge" +} from '@/components/ui/overlay/dropdown-menu'; +import { Badge } from '@/components/ui/common/badge'; +import { useTranslations } from 'next-intl'; export type FilterOption = { - id: string - label: string - checked: boolean -} + id: string; + label: string; + checked: boolean; +}; export interface FilterDropdownProps { - options: FilterOption[] - onChange: (options: FilterOption[]) => void - label?: string - buttonText?: string - showActiveBadges?: boolean - showActiveCount?: boolean - className?: string - isOpen?: boolean - onOpenChange?: (open: boolean) => void + options: FilterOption[]; + onChange: (options: FilterOption[]) => void; + label?: string; + buttonText?: string; + showActiveBadges?: boolean; + showActiveCount?: boolean; + className?: string; + isOpen?: boolean; + onOpenChange?: (open: boolean) => void; } export function FilterDropdown({ options, onChange, - label = "Filter by status", - buttonText = "Filter", + label = 'Filter by status', + buttonText = 'Filter', showActiveBadges = true, showActiveCount = true, - className = "", + className = '', isOpen: controlledIsOpen, onOpenChange, }: FilterDropdownProps) { // Use internal state if not controlled externally - const [internalIsOpen, setInternalIsOpen] = useState(false) - const isOpen = controlledIsOpen !== undefined ? controlledIsOpen : internalIsOpen + const [internalIsOpen, setInternalIsOpen] = useState(false); + const isOpen = + controlledIsOpen !== undefined ? controlledIsOpen : internalIsOpen; + const t = useTranslations('dashboard.proposals.filterOptions'); // Sync local options with props - const [localOptions, setLocalOptions] = useState(options) + const [localOptions, setLocalOptions] = useState(options); useEffect(() => { - setLocalOptions(options) - }, [options]) + setLocalOptions(options); + }, [options]); - const activeFiltersCount = localOptions.filter((option) => option.checked).length + const activeFiltersCount = localOptions.filter( + (option) => option.checked, + ).length; const handleOpenChange = (open: boolean) => { if (controlledIsOpen === undefined) { - setInternalIsOpen(open) + setInternalIsOpen(open); } - onOpenChange?.(open) - } + onOpenChange?.(open); + }; const toggleFilter = (id: string) => { const updatedOptions = localOptions.map((option) => option.id === id ? { ...option, checked: !option.checked } : option, - ) - setLocalOptions(updatedOptions) - onChange(updatedOptions) - } + ); + setLocalOptions(updatedOptions); + onChange(updatedOptions); + }; const clearFilters = () => { - const clearedOptions = localOptions.map((option) => ({ ...option, checked: false })) - setLocalOptions(clearedOptions) - onChange(clearedOptions) - } + const clearedOptions = localOptions.map((option) => ({ + ...option, + checked: false, + })); + setLocalOptions(clearedOptions); + onChange(clearedOptions); + }; return (
- +
@@ -134,7 +146,11 @@ export function FilterDropdown({ {localOptions .filter((option) => option.checked) .map((option) => ( - + {option.label}
- ) + ); } diff --git a/frontend/vizzy/messages/en.json b/frontend/vizzy/messages/en.json index 6b203702..ca32c805 100644 --- a/frontend/vizzy/messages/en.json +++ b/frontend/vizzy/messages/en.json @@ -135,12 +135,22 @@ }, "memberSince": { "label": "Member Since" - } + }, + "noTransactions": "No transactions yet", + "memberSinceNotAvailable": "Member since not available" }, "listingsSection": { "title": "Active Listings", "seeAll": "See all", "noActiveListings": "There are no active listings." + }, + "location": { + "notAvailable": "Location not available" + }, + "contacts": { + "loading": "Loading contacts...", + "error": "Failed to load contacts", + "noContacts": "No contacts available" } }, "listing": { @@ -281,6 +291,21 @@ "rejected": "Rejected", "cancelled": "Cancelled" }, + "proposalDetails": { + "title": "Proposal Details", + "swapWith": "Swap with", + "proposalFrom": "Proposal from", + "proposalImages": "Proposal Images", + "listingFrom": "Listing from", + "images": "Proposal Images", + "seeImage": "See image", + "valuePerDay": "Value per Day", + "rentalPeriod": "Rental Period", + "totalValue": "Total Value", + "proposedPrice": "Proposed Price", + "days": "days", + "listingInformation": "Listing Information" + }, "back": "← Back to Proposals", "rentalDialog": { "title": "Rental Proposal", @@ -339,6 +364,30 @@ } } }, + "dashboard": { + "title": "Dashboard", + "subtitle": "Manage Listings and Proposals", + "tabs": { + "overview": "Overview", + "listings": "Listings", + "proposals": "Proposals" + }, + "listings": { + "title": "Listings", + "button": "New Listing" + }, + "proposals": { + "title": "Proposals", + "filters": "Filters", + "filterOptions": { + "received": "Received", + "sent": "Sent", + "pending": "Pending", + "accepted": "Accepted", + "rejected": "Rejected", + "cancelled": "Cancelled", + "clearFilters": "Clear Filters" + } "favoritesPage": { "error": { "loadFailed": "Failed to load favorites.", diff --git a/frontend/vizzy/messages/pt.json b/frontend/vizzy/messages/pt.json index af4a58c2..bc3901af 100644 --- a/frontend/vizzy/messages/pt.json +++ b/frontend/vizzy/messages/pt.json @@ -135,12 +135,22 @@ }, "memberSince": { "label": "Membro Desde" - } + }, + "noTransactions": "Não existem transações ainda.", + "memberSinceNotAvailable": "Membro desde não disponível" }, "listingsSection": { "title": "Anúncios Atuais", "seeAll": "Ver todos", "noActiveListings": "Não existem anúncios atuais." + }, + "location": { + "notAvailable": "Localização não disponível" + }, + "contacts": { + "loading": "A carregar contactos...", + "error": "Falha ao carregar contactos", + "noContacts": "Não existem contactos disponíveis" } }, "listing": { @@ -281,6 +291,21 @@ "rejected": "Rejeitado", "cancelled": "Cancelado" }, + "proposalDetails": { + "title": "Informações da Proposta", + "swapWith": "Item para troca", + "proposalFrom": "Proposta de", + "proposalImages": "Imagens em Anexo", + "listingFrom": "Anúncio de", + "images": "Imagens em Anexo", + "seeImage": "Ver imagem", + "valuePerDay": "Valor por Dia", + "rentalPeriod": "Período de Aluguer", + "totalValue": "Valor Total", + "proposedPrice": "Valor Proposto", + "days": "dias", + "listingInformation": "Informações do Anúncio" + }, "back": "← Voltar às Propostas", "rentalDialog": { "title": "Proposta de Aluguer", @@ -339,6 +364,34 @@ } } }, + "dashboard": { + "title": "Painel de Controlo", + "subtitle": "Gestão de Anúncios e Propostas", + "tabs": { + "overview": "Visão Geral", + "listings": "Anúncios", + "proposals": "Propostas" + }, + "overview": { + "title": "Visão Geral", + "description": "Resumo das transações e propostas recentes." + }, + "listings": { + "title": "Anúncios", + "button": "Novo Anúncio" + }, + "proposals": { + "title": "Propostas", + "filters": "Filtros", + "filterOptions": { + "received": "Recebidas", + "sent": "Enviadas", + "pending": "Pendentes", + "accepted": "Aceites", + "rejected": "Rejeitadas", + "cancelled": "Canceladas", + "clearFilters": "Limpar Filtros" + } "favoritesPage": { "error": { "loadFailed": "Falha ao carregar favoritos.",