diff --git a/messages/en.json b/messages/en.json index 79d156c..ad35618 100644 --- a/messages/en.json +++ b/messages/en.json @@ -249,5 +249,201 @@ "notes": "Notes", "deals": "Deals" } + }, + "Contacts": { + "page": { + "title": "Contacts", + "subtitle": "Manage your contact list. {count} total contacts.", + "subtitleZero": "Manage your contact list.", + "customFieldsBtn": "Custom fields", + "importBtn": "Import", + "addContactBtn": "Add Contact", + "searchPlaceholder": "Search by name, phone, or email...", + "filterByTags": "Filter by tags", + "clearAll": "Clear all", + "noTagsYet": "No tags yet.", + "selectedCount": "{count} selected", + "clearSelection": "Clear", + "deleteSelected": "Delete selected", + "tableColumns": { + "name": "Name", + "phone": "Phone", + "email": "Email", + "company": "Company", + "tags": "Tags", + "createdAt": "Created" + }, + "loading": "Loading contacts...", + "noContactsMatch": "No contacts match your filters.", + "noContactsYet": "No contacts yet.", + "addFirstContact": "Add your first contact", + "unnamed": "Unnamed", + "editAction": "Edit", + "deleteAction": "Delete", + "showingPagination": "Showing {start}-{end} of {total}", + "pageCount": "Page {page} of {total}", + "deleteContactTitle": "Delete Contact", + "deleteContactDesc": "Are you sure you want to delete {name}? This action cannot be undone.", + "cancel": "Cancel", + "deleteBtn": "Delete", + "deleteBulkTitle": "Delete Contacts", + "deleteBulkDesc": "Are you sure you want to delete {count} selected contacts? This action cannot be undone.", + "toastFailedLoad": "Failed to load contacts", + "toastFailedDelete": "Failed to delete contact", + "toastDeleted": "Contact deleted", + "toastBulkFailedDelete": "Failed to delete contacts", + "toastBulkDeleted": "{count} contacts deleted" + }, + "form": { + "addTitle": "Add Contact", + "addDesc": "Create a new contact. Name is required.", + "editTitle": "Edit Contact", + "editDesc": "Update contact details and tags.", + "nameLabel": "Name", + "namePlaceholder": "Jane Doe", + "phoneLabel": "Phone Number", + "phonePlaceholder": "+1234567890", + "phoneHint": "Include country code, e.g. +1 for US", + "emailLabel": "Email", + "emailPlaceholder": "jane@example.com", + "companyLabel": "Company", + "companyPlaceholder": "Acme Corp", + "tagsLabel": "Tags", + "tagsPlaceholder": "Select tags...", + "noTagsFound": "No tags found.", + "createTagAction": "Create tag \"{value}\"", + "cancel": "Cancel", + "save": "Save Contact", + "update": "Update", + "create": "Create", + "toastSuccessAdd": "Contact created successfully", + "toastSuccessEdit": "Contact updated successfully", + "toastError": "Failed to save contact", + "toastConflict": "A contact with this phone number already exists", + "viewExisting": "View {name}", + "dupExact": "A contact with this phone number already exists.", + "dupSimilar": "A contact with a very similar number already exists.", + "loadingTags": "Loading tags...", + "noTagsAvailable": "No tags available. Create tags in Settings.", + "phoneRequired": "Phone number is required" + }, + "detailView": { + "tabs": { + "details": "Details", + "activity": "Activity", + "notes": "Notes", + "custom": "Custom Fields", + "deals": "Deals" + }, + "actions": { + "edit": "Edit", + "message": "Message", + "delete": "Delete" + }, + "contactInfo": "Contact Information", + "unnamed": "Unknown", + "contactDetailsDesc": "Contact details", + "phone": "Phone", + "email": "Email", + "company": "Company", + "createdAt": "Created At", + "customFields": "Custom Fields", + "noCustomFields": "No custom fields defined. Create them in Settings.", + "manageFields": "Manage fields", + "enterCustomField": "Enter {name}...", + "activityTab": { + "title": "Activity History", + "noActivity": "No activity yet." + }, + "notesTab": { + "title": "Internal Notes", + "placeholder": "Write a note...", + "save": "Add Note", + "noNotes": "No notes yet." + }, + "dealsTab": { + "noDeals": "No deals yet" + }, + "tagsTab": { + "clickTagDesc": "Click a tag to add or remove it from this contact.", + "noTagsAvailable": "No tags available. Create tags in Settings." + }, + "sendTemplateBtn": "Send template", + "saveChangesBtn": "Save Changes", + "saveCustomFieldsBtn": "Save Custom Fields", + "toastPhoneRequired": "Phone number is required", + "toastUpdateFailed": "Failed to update contact", + "toastUpdated": "Contact updated", + "toastNotAuthenticated": "Not authenticated", + "toastNoteAddFailed": "Failed to add note", + "toastNoteAdded": "Note added", + "toastNoteDeleteFailed": "Failed to delete note", + "toastNoteDeleted": "Note deleted", + "toastCustomFieldsSaved": "Custom fields saved", + "toastCustomFieldsFailed": "Failed to save custom fields", + "toastTemplateSent": "Template \"{name}\" sent", + "toastTemplateFailed": "Failed to send template: {reason}" + }, + "importModal": { + "title": "Import Contacts", + "desc": "Upload a CSV with a required phone column. Optional: name, email, company, tags (comma-separated; quote multi-tag cells).", + "uploadDropzone": "Click to choose a CSV file", + "uploadHint": ".csv up to your browser limit", + "rowsReady": "{count} row ready", + "rowsReady_plural": "{count} rows ready", + "preview": "Preview · first {count}", + "previewTags": "{tags} tag · {contacts} contact", + "previewTags_plural": "{tags} tags · {contacts} contacts", + "moreRows": "+ {count} more row not shown", + "moreRows_plural": "+ {count} more rows not shown", + "importComplete": "Import complete", + "resultImported": "{count} imported", + "resultTags": "{count} tag assigned", + "resultTags_plural": "{count} tags assigned", + "resultSkipped": "{count} skipped", + "resultFailed": "{count} failed", + "willBeCreated": "{name} (will be created on import)", + "columns": { + "phone": "Phone", + "name": "Name", + "email": "Email", + "company": "Company", + "tags": "Tags" + }, + "cancel": "Cancel", + "close": "Close", + "importBtn": "Import {count} contact", + "importBtn_plural": "Import {count} contacts", + "toastNoValidRows": "No valid rows found. Ensure CSV has a \"phone\" column header.", + "toastTagsWarning": "Contacts imported, but some tag assignments failed.", + "toastImported": "{count} contact imported", + "toastImported_plural": "{count} contacts imported", + "toastTagsAssigned": "{count} tag assignment applied", + "toastTagsAssigned_plural": "{count} tag assignments applied", + "toastTagsSkipped": "Unknown tags skipped (create them in Settings first): {sample}{more}", + "toastSkipped": "{count} duplicate skipped", + "toastSkipped_plural": "{count} duplicates skipped", + "toastFailed": "{count} contact failed to import", + "toastFailed_plural": "{count} contacts failed to import", + "toastError": "Import failed" + }, + "customFields": { + "title": "Custom fields", + "desc": "Define extra contact fields (e.g. ZIP code, lead source). They appear on every contact and in the “Update Contact Field” automation action.", + "addField": "Add", + "fieldName": "New field name…", + "loading": "Loading…", + "empty": "No custom fields yet.", + "renameAria": "Rename {name}", + "deleteTitle": "Delete field", + "deleteConfirm": "Delete \"{name}\"? This also removes its stored value on every contact. This cannot be undone.", + "toastNoAccount": "Your profile is not linked to an account.", + "toastDuplicate": "A field named \"{name}\" already exists.", + "toastCreateFailed": "Could not create field. You may not have permission.", + "toastCreated": "Created \"{name}\".", + "toastRenameFailed": "Could not rename field.", + "toastDeleteFailed": "Could not delete field.", + "toastDeleted": "Deleted \"{name}\"." + } } } diff --git a/src/app/(dashboard)/contacts/page.tsx b/src/app/(dashboard)/contacts/page.tsx index 5d0bc9c..0facba6 100644 --- a/src/app/(dashboard)/contacts/page.tsx +++ b/src/app/(dashboard)/contacts/page.tsx @@ -14,6 +14,7 @@ import { TableHeader, TableRow, } from '@/components/ui/table'; +import { Checkbox } from '@/components/ui/checkbox'; import { DropdownMenu, DropdownMenuTrigger, @@ -55,7 +56,7 @@ import { ImportModal } from '@/components/contacts/import-modal'; import { CustomFieldsManager } from '@/components/contacts/custom-fields-manager'; import { useCan } from '@/hooks/use-can'; import { GatedButton } from '@/components/ui/gated-button'; -import { Checkbox } from '@/components/ui/checkbox'; +import { useTranslations } from 'next-intl'; const PAGE_SIZE = 25; @@ -64,6 +65,7 @@ interface ContactWithTags extends Contact { } export default function ContactsPage() { + const t = useTranslations('Contacts.page'); const supabase = createClient(); const canEdit = useCan('send-messages'); const canEditSettings = useCan('edit-settings'); @@ -144,7 +146,7 @@ export default function ContactsPage() { }); if (seq !== fetchSeq.current) return; // superseded by a newer fetch if (error) { - toast.error('Failed to load contacts'); + toast.error(t('toastFailedLoad')); setLoading(false); return; } @@ -166,7 +168,7 @@ export default function ContactsPage() { const { data, count: exactCount, error } = await query; if (seq !== fetchSeq.current) return; // superseded by a newer fetch if (error) { - toast.error('Failed to load contacts'); + toast.error(t('toastFailedLoad')); setLoading(false); return; } @@ -205,7 +207,7 @@ export default function ContactsPage() { setContacts(enriched); setLoading(false); - }, [supabase, page, search, selectedTagIds, tagsMap]); + }, [supabase, page, search, selectedTagIds, tagsMap, t]); // Load-once-on-mount-ish data fetches. Each setter inside runs // inside an async promise completion (Supabase await), not @@ -257,9 +259,9 @@ export default function ContactsPage() { .eq('id', deleteTarget.id); if (error) { - toast.error('Failed to delete contact'); + toast.error(t('toastFailedDelete')); } else { - toast.success('Contact deleted'); + toast.success(t('toastDeleted')); fetchContacts(); } @@ -301,9 +303,9 @@ export default function ContactsPage() { const { error } = await supabase.from('contacts').delete().in('id', ids); if (error) { - toast.error('Failed to delete contacts'); + toast.error(t('toastBulkFailedDelete')); } else { - toast.success(`${ids.length} contact${ids.length === 1 ? '' : 's'} deleted`); + toast.success(t('toastBulkDeleted', { count: ids.length })); setSelected(new Set()); fetchContacts(); } @@ -342,9 +344,9 @@ export default function ContactsPage() { {/* Header */}
-

Contacts

+

{t('title')}

- Manage your contact list. {totalCount > 0 && `${totalCount} total contacts.`} + {totalCount > 0 ? t('subtitle', { count: totalCount }) : t('subtitleZero')}

@@ -355,7 +357,7 @@ export default function ContactsPage() { className="border-border text-muted-foreground hover:bg-muted" > - Custom fields + {t('customFieldsBtn')} )} - Import + {t('importBtn')} - Add Contact + {t('addContactBtn')}
@@ -393,7 +395,7 @@ export default function ContactsPage() { // set shrinks/grows, page N may no longer be valid. setPage(0); }} - placeholder="Search by name, phone, or email..." + placeholder={t('searchPlaceholder')} className="pl-8 bg-card border-border text-foreground placeholder:text-muted-foreground" /> @@ -408,7 +410,7 @@ export default function ContactsPage() { } > - Filter by tags + {t('filterByTags')} {selectedTagIds.length > 0 && ( {selectedTagIds.length} @@ -418,20 +420,20 @@ export default function ContactsPage() {
- Filter by tags + {t('filterByTags')} {selectedTagIds.length > 0 && ( )}
{allTags.length === 0 ? (

- No tags yet. + {t('noTagsYet')}

) : (
@@ -490,7 +492,7 @@ export default function ContactsPage() { onClick={clearTagFilters} className="text-xs text-muted-foreground hover:text-foreground px-1" > - Clear all + {t('clearAll')}
)} @@ -500,8 +502,7 @@ export default function ContactsPage() { {selected.size > 0 && (

- {selected.size}{' '} - {selected.size === 1 ? 'contact' : 'contacts'} selected + {t('selectedCount', { count: selected.size })}

setBulkDeleteOpen(true)} > - Delete selected + {t('deleteSelected')}
@@ -540,12 +541,12 @@ export default function ContactsPage() { aria-label="Select all contacts on this page" /> - Name - Phone - Email - Company - Tags - Created + {t('tableColumns.name')} + {t('tableColumns.phone')} + {t('tableColumns.email')} + {t('tableColumns.company')} + {t('tableColumns.tags')} + {t('tableColumns.createdAt')} @@ -555,7 +556,7 @@ export default function ContactsPage() {
-

Loading contacts...

+

{t('loading')}

@@ -566,8 +567,8 @@ export default function ContactsPage() {

{hasActiveFilters - ? 'No contacts match your filters.' - : 'No contacts yet.'} + ? t('noContactsMatch') + : t('noContactsYet')}

{!hasActiveFilters && ( - Add your first contact + {t('addFirstContact')} )} @@ -600,7 +601,7 @@ export default function ContactsPage() { /> - {contact.name || Unnamed} + {contact.name || {t('unnamed')}} {contact.phone} @@ -669,7 +670,7 @@ export default function ContactsPage() { className="text-popover-foreground focus:bg-muted focus:text-foreground" > - Edit + {t('editAction')} - Delete + {t('deleteAction')} @@ -696,8 +697,11 @@ export default function ContactsPage() { {totalPages > 1 && (

- Showing {page * PAGE_SIZE + 1}-{Math.min((page + 1) * PAGE_SIZE, totalCount)} of{' '} - {totalCount} + {t('showingPagination', { + start: page * PAGE_SIZE + 1, + end: Math.min((page + 1) * PAGE_SIZE, totalCount), + total: totalCount + })}

- Page {page + 1} of {totalPages} + {t('pageCount', { page: page + 1, total: totalPages })} @@ -802,14 +802,10 @@ export default function ContactsPage() { - Delete {selected.size} {selected.size === 1 ? 'Contact' : 'Contacts'} + {t('deleteBulkTitle')} - Are you sure you want to delete{' '} - - {selected.size} {selected.size === 1 ? 'contact' : 'contacts'} - - ? This action cannot be undone. + {t('deleteBulkDesc', { count: selected.size })} @@ -818,7 +814,7 @@ export default function ContactsPage() { onClick={() => setBulkDeleteOpen(false)} className="border-border text-muted-foreground hover:bg-muted" > - Cancel + {t('cancel')} diff --git a/src/components/contacts/contact-detail-view.tsx b/src/components/contacts/contact-detail-view.tsx index 35a3982..5d50424 100644 --- a/src/components/contacts/contact-detail-view.tsx +++ b/src/components/contacts/contact-detail-view.tsx @@ -39,6 +39,7 @@ import { DollarSign, LayoutTemplate, } from 'lucide-react'; +import { useTranslations } from 'next-intl'; interface ContactDetailViewProps { open: boolean; @@ -53,6 +54,7 @@ export function ContactDetailView({ contactId, onUpdated, }: ContactDetailViewProps) { + const t = useTranslations('Contacts.detailView'); const supabase = createClient(); const { accountId, defaultCurrency } = useAuth(); @@ -196,7 +198,7 @@ export function ContactDetailView({ async function saveDetails() { if (!contactId || !editPhone.trim()) { - toast.error('Phone number is required'); + toast.error(t('toastPhoneRequired')); return; } @@ -213,9 +215,9 @@ export function ContactDetailView({ .eq('id', contactId); if (error) { - toast.error('Failed to update contact'); + toast.error(t('toastUpdateFailed')); } else { - toast.success('Contact updated'); + toast.success(t('toastUpdated')); fetchContact(); onUpdated(); } @@ -259,7 +261,7 @@ export function ContactDetailView({ } = await supabase.auth.getSession(); const user = session?.user; if (!user || !accountId) { - toast.error('Not authenticated'); + toast.error(t('toastNotAuthenticated')); setSavingNote(false); return; } @@ -272,11 +274,11 @@ export function ContactDetailView({ }); if (error) { - toast.error('Failed to add note'); + toast.error(t('toastNoteAddFailed')); } else { setNewNote(''); fetchNotes(); - toast.success('Note added'); + toast.success(t('toastNoteAdded')); } setSavingNote(false); } @@ -288,10 +290,10 @@ export function ContactDetailView({ .eq('id', noteId); if (error) { - toast.error('Failed to delete note'); + toast.error(t('toastNoteDeleteFailed')); } else { setNotes((prev) => prev.filter((n) => n.id !== noteId)); - toast.success('Note deleted'); + toast.success(t('toastNoteDeleted')); } } @@ -321,9 +323,9 @@ export function ContactDetailView({ if (error) throw error; } - toast.success('Custom fields saved'); + toast.success(t('toastCustomFieldsSaved')); } catch { - toast.error('Failed to save custom fields'); + toast.error(t('toastCustomFieldsFailed')); } setSavingCustom(false); } @@ -357,11 +359,11 @@ export function ContactDetailView({ const payload = await res.json().catch(() => ({})); if (!res.ok) { const reason = payload?.error || `HTTP ${res.status}`; - toast.error(`Failed to send template: ${reason}`); + toast.error(t('toastTemplateFailed', { reason })); return; } - toast.success(`Template "${template.name}" sent`); + toast.success(t('toastTemplateSent', { name: template.name })); } catch (err) { const reason = err instanceof Error ? err.message : 'network error'; toast.error(`Failed to send template: ${reason}`); @@ -403,10 +405,10 @@ export function ContactDetailView({
- {contact.name || 'Unknown'} + {contact.name || t('unnamed')} - Contact details + {t('contactDetailsDesc')}
@@ -460,31 +462,31 @@ export function ContactDetailView({ value="details" className="data-active:bg-muted data-active:text-primary text-muted-foreground" > - Details + {t('tabs.details')} - Tags + {t('tabs.tags', { fallback: 'Tags' })} - Notes + {t('tabs.notes')} - Custom Fields + {t('tabs.custom')} - Deals + {t('tabs.deals')} @@ -492,7 +494,7 @@ export function ContactDetailView({
- + setEditName(e.target.value)} @@ -501,7 +503,7 @@ export function ContactDetailView({
- + setEditEmail(e.target.value)} @@ -518,7 +520,7 @@ export function ContactDetailView({ />
- + setEditCompany(e.target.value)} @@ -536,7 +538,7 @@ export function ContactDetailView({ ) : ( )} - Save Changes + {t('saveChangesBtn')}
@@ -545,11 +547,11 @@ export function ContactDetailView({

- Click a tag to add or remove it from this contact. + {t('tagsTab.clickTagDesc')}

{allTags.length === 0 ? (

- No tags available. Create tags in Settings. + {t('tagsTab.noTagsAvailable')}

) : (
@@ -586,7 +588,7 @@ export function ContactDetailView({